"""List pages must load without a banner that reflows the page. Every list refresh used to raise an info banner ("正在加载…") into the page layout and drop it again on completion. That is two relayouts per refresh - the filter bar and the whole table jump down and back - and the appointment and consultation pages poll every five seconds, so the jump repeated on its own with nobody touching the keyboard. Errors still surface on the banner; only the routine load is silent. """ from __future__ import annotations import os from types import SimpleNamespace from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtWidgets import QApplication from doctor_workstation.core import PermissionSet from doctor_workstation.services import DemoDoctorRepository from doctor_workstation.ui.pages import appointments as appointments_module from doctor_workstation.ui.pages import prescription_library as library_module from doctor_workstation.ui.pages import prescriptions as prescriptions_module from doctor_workstation.ui.pages.appointments import AppointmentsPage from doctor_workstation.ui.pages.prescription_library import PrescriptionLibraryPage from doctor_workstation.ui.pages.prescriptions import PrescriptionsPage @pytest.fixture(scope="module") def application() -> QApplication: return QApplication.instance() or QApplication([]) @pytest.fixture(autouse=True) def immediate_async(monkeypatch: pytest.MonkeyPatch) -> None: """Run the worker inline so the banner state can be inspected mid-load.""" def run_immediately( function: Any, *args: Any, on_success: Any = None, on_error: Any = None, on_finished: Any = None, **kwargs: Any, ) -> object: try: result = function(*args, **kwargs) except Exception as error: # noqa: BLE001 - mirrors the real worker 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() return object() for module in (prescriptions_module, library_module, appointments_module): monkeypatch.setattr(module, "run_async", run_immediately) def _pages(application: QApplication) -> list[Any]: del application repository = DemoDoctorRepository() return [ PrescriptionsPage(repository, ["*"]), PrescriptionLibraryPage( repository, ["*"], SimpleNamespace(id=1, root=1, role_ids=[]) ), AppointmentsPage(repository, PermissionSet(["*"])), ] def test_a_list_refresh_never_raises_a_loading_banner( application: QApplication, ) -> None: seen: list[tuple[str, str]] = [] for page in _pages(application): banner = page.banner original = banner.show_message def record(text: str, kind: str = "info", _page: Any = page) -> None: seen.append((type(_page).__name__, text)) banner.show_message = record # type: ignore[method-assign] page.refresh() application.processEvents() banner.show_message = original # type: ignore[method-assign] page.close() assert seen == [], f"a list refresh still announced itself: {seen}" def test_a_refresh_does_not_move_anything_on_the_page( application: QApplication, ) -> None: """The real symptom was reflow, so assert on geometry rather than on a flag. A hidden widget keeps whatever size it was last given, so checking the banner's own height proves nothing; what matters is that the table below it does not shift when a refresh runs. """ for page in _pages(application): page.resize(1200, 700) page.show() application.processEvents() table = page.table settled = (table.mapTo(page, table.rect().topLeft()), table.size()) for _ in range(3): page.refresh() application.processEvents() assert not page.banner.isVisible() assert (table.mapTo(page, table.rect().topLeft()), table.size()) == settled page.close() application.processEvents() def test_a_failed_load_still_reports_on_the_banner( application: QApplication, ) -> None: """Silence is only for the happy path; failures must stay visible.""" class Failing(DemoDoctorRepository): def list_prescriptions(self, **_filters: Any) -> Any: raise RuntimeError("上游不可用") page = PrescriptionsPage(Failing(), ["*"]) page.resize(1200, 700) page.show() page.refresh() application.processEvents() assert page.banner.isVisible() assert page.banner.label.text() page.close() application.processEvents()