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

157 lines
6.5 KiB
Python

from __future__ import annotations
import os
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QPoint, QTimer
from PySide6.QtWidgets import QApplication, QLabel, QPushButton
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui import apply_theme
from doctor_workstation.ui.pages import (
appointments,
consultations,
patients,
prescription_library,
prescriptions,
)
from doctor_workstation.ui.widgets import PageHeader
@pytest.fixture(scope="module")
def application() -> QApplication:
app = QApplication.instance() or QApplication([])
apply_theme(app)
return app
@pytest.fixture
def page_factory(application: QApplication, monkeypatch: pytest.MonkeyPatch):
def immediate(function: Any, *args: Any, on_success=None, on_error=None,
on_finished=None, **kwargs: Any) -> object:
try:
result = function(*args, **kwargs)
except Exception as error:
if on_error:
on_error(error)
else:
if on_success:
on_success(result)
finally:
if on_finished:
on_finished()
return object()
modules = (appointments, consultations, patients, prescription_library, prescriptions)
for module in modules:
monkeypatch.setattr(module, "run_async", immediate)
opened = []
def create(kind: str, width: int = 1040):
repo = DemoDoctorRepository()
session = repo.login(repo.DEMO_ACCOUNT, repo.DEMO_PASSWORD)
classes = {
"appointments": appointments.AppointmentsPage,
"patients": patients.PatientsPage,
"consultations": consultations.ConsultationsPage,
"prescriptions": prescriptions.PrescriptionsPage,
"prescription_library": prescription_library.PrescriptionLibraryPage,
}
page = classes[kind](repo, permissions=session.permissions, current_user=session.user)
opened.append(page)
page.resize(width, 700)
page.show()
page.refresh()
for _ in range(4):
application.processEvents()
return page
yield create
for page in opened:
for timer in page.findChildren(QTimer):
timer.stop()
page.close()
page.deleteLater()
application.processEvents()
@pytest.mark.parametrize("kind", ["appointments", "patients", "consultations", "prescriptions", "prescription_library"])
def test_list_header_fits_the_real_bundled_font(page_factory, kind: str) -> None:
page = page_factory(kind)
header = page.findChild(PageHeader)
assert header is not None
for label in header.findChildren(QLabel):
if label.isVisible() and label.text() and label.property("role"):
assert label.height() >= label.fontMetrics().height(), (
kind, label.text(), label.height(), label.fontMetrics().height()
)
@pytest.mark.parametrize("kind", ["patients", "prescription_library"])
@pytest.mark.parametrize("width", [1040, 1200])
def test_row_actions_are_fully_reachable_at_each_width(page_factory, kind, width):
page = page_factory(kind, width)
table = page.patient_workspace.table if kind == "patients" else page.table
assert table.rowCount() > 0
assert table.horizontalHeader().visualIndex(9) == 9
actions = table.cellWidget(0, 9)
assert actions is not None
assert table.horizontalScrollBar().value() == 0
# Both approved layouts put actions after the nine data columns. Preserve
# full-size controls and the existing reachability assertion after scrolling.
table.horizontalScrollBar().setValue(table.horizontalScrollBar().maximum())
QApplication.processEvents()
assert actions.geometry().left() >= 0
assert actions.geometry().right() < table.viewport().width()
assert actions.height() >= actions.minimumSizeHint().height()
for child in actions.findChildren(QPushButton):
assert actions.rect().contains(child.mapTo(actions, QPoint()))
assert actions.rect().contains(child.mapTo(actions, child.rect().bottomRight()))
def test_appointment_detail_lines_and_cancel_fit_without_overlapping(page_factory) -> None:
page = page_factory("appointments")
table = page.table
assert table.rowCount() > 0
host = table.cellWidget(0, 4)
assert host.width() >= host.minimumSizeHint().width()
assert host.height() >= host.minimumSizeHint().height()
for label in host.findChildren(QLabel):
assert label.height() >= label.fontMetrics().height()
assert host.rect().contains(label.geometry())
cancel = next(button for button in host.findChildren(QPushButton) if button.text() == "取消")
for label in host.findChildren(QLabel):
assert not label.geometry().intersects(cancel.geometry())
# Underlying text is still available to sorting while a delegate paints only
# the selection surface beneath the actual controls.
assert table.item(0, 4).text()
assert isinstance(table.itemDelegateForColumn(4), appointments._AppointmentInfoDelegate)
assert table.horizontalScrollBar().maximum() == 0
search = page.findChild(QPushButton, "AppointmentSearchButton")
assert search.height() == page.patient_input.height()
assert all(button.height() == search.height() for button in page.date_buttons.values() if button.isVisible())
@pytest.mark.parametrize("kind", ["patients", "prescriptions", "prescription_library", "consultations"])
def test_filter_actions_align_with_their_inputs(page_factory, kind: str) -> None:
page = page_factory(kind)
if kind == "patients":
controls = page.patient_workspace
widgets = (controls.keyword_edit, controls.search_button, controls.reset_button)
statuses = tuple(controls.status_buttons.values())
assert len({widget.height() for widget in statuses}) == 1
assert len({widget.mapTo(controls.status_host, QPoint()).y() for widget in statuses}) == 1
for widget in statuses:
assert controls.status_host.rect().contains(widget.geometry())
assert widget.height() >= widget.fontMetrics().height()
elif kind == "prescriptions":
widgets = (page.sn_filter, page.query_button, page.reset_button, page.doctor_filter.button)
elif kind == "prescription_library":
widgets = (page.name_filter, page.query_button, page.reset_button)
else:
widgets = (page.search_button, page.reset_button, page.custom_date_edit)
assert len({widget.height() for widget in widgets}) == 1