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