146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""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()
|