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.QtCore import Qt from PySide6.QtGui import QImage from PySide6.QtWidgets import QApplication, QStyleOptionViewItem from doctor_workstation.ui.pages import prescriptions as prescriptions_module from doctor_workstation.ui.pages.prescriptions import PrescriptionsPage, _order_warnings @pytest.fixture(scope="module") def application() -> QApplication: return QApplication.instance() or QApplication([]) @pytest.fixture(autouse=True) def immediate_async(monkeypatch: pytest.MonkeyPatch) -> None: 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: 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() monkeypatch.setattr(prescriptions_module, "run_async", run_immediately) def row( record_id: int, number: str, *, has_order: Any, herbs: Any, ) -> dict[str, Any]: return { "id": record_id, "sn": number, "patient_name": "列表提示测试患者", "gender": 1, "age": 45, "prescription_type": "浓缩水丸", "is_system_auto": 1, "audit_status": 0, "void_status": 0, "has_prescription_order": has_order, "herbs": herbs, "doctor_name": "测试医生", "assistant_name": "测试医助", "create_time": "2026-09-01 09:30:00", } class Repository: def __init__(self, rows: list[dict[str, Any]]) -> None: self.rows = rows def list_diagnosis_doctors(self) -> list[Any]: return [] def list_prescriptions(self, **_filters: Any) -> dict[str, Any]: return {"lists": self.rows, "count": len(self.rows)} def test_order_warnings_exactly_match_pc_rules() -> None: assert _order_warnings(row(1, "RX-1", has_order=0, herbs=[])) == [] assert _order_warnings(row(2, "RX-2", has_order=1, herbs=[])) == ["请开方,当前处方药材为空白"] assert _order_warnings( row(3, "RX-3", has_order="1.0", herbs=[{}, {"name": ""}, {"name": " "}]) ) == ["请开方,当前处方药材为空白"] assert _order_warnings( row( 4, "RX-4", has_order=1, herbs=[{"name": "黄芪"}, {"name": " 黄 芪 "}, {"name": "党参"}], ) ) == ["已有关联业务订单,当前处方存在重复药材:黄 芪"] # The PC implementation only reads the canonical `name` field. assert _order_warnings(row(5, "RX-5", has_order=1, herbs=[{"medicine_name": "黄芪"}])) == [ "请开方,当前处方药材为空白" ] def test_number_column_renders_sn_id_and_visible_warning_with_dynamic_height( application: QApplication, ) -> None: rows = [ row(11, "RX-NORMAL", has_order=0, herbs=[]), row(22, "RX-BLANK", has_order=1, herbs=[{"name": ""}]), row( 33, "RX-DUPLICATE", has_order=1, herbs=[{"name": "黄芪"}, {"name": "黄 芪"}], ), ] page = PrescriptionsPage(Repository(rows), {"*"}, SimpleNamespace(id=7, name="测试医生")) page.resize(1366, 768) page.show() page.refresh() for _ in range(6): application.processEvents() rendered: dict[int, tuple[int, str, str]] = {} for visual_row in range(page.table.rowCount()): item = page.table.item(visual_row, 1) source = item.data(Qt.ItemDataRole.UserRole) rendered[source["id"]] = ( page.table.rowHeight(visual_row), item.toolTip(), item.data(Qt.ItemDataRole.AccessibleTextRole), ) normal_height, normal_tip, normal_accessible = rendered[11] blank_height, blank_tip, blank_accessible = rendered[22] duplicate_height, duplicate_tip, _duplicate_accessible = rendered[33] assert normal_tip == normal_accessible == "RX-NORMAL\nID: 11" assert "请开方,当前处方药材为空白" in blank_tip == blank_accessible assert "已有关联业务订单,当前处方存在重复药材:黄 芪" in duplicate_tip assert blank_height > normal_height assert duplicate_height > normal_height assert page.table.columnWidth(1) == 192 image = page.table.viewport().grab().toImage().convertToFormat(QImage.Format.Format_RGB32) red_pixels = 0 for y in range(image.height()): for x in range(image.width()): color = image.pixelColor(x, y) if color.red() > 170 and color.green() < 90 and color.blue() < 100: red_pixels += 1 assert red_pixels > 40 page.close() application.processEvents() def test_number_warning_survives_sort_and_column_resize(application: QApplication) -> None: rows = [ row(41, "RX-Z", has_order=1, herbs=[]), row(42, "RX-A", has_order=0, herbs=[{"name": "黄芪"}]), ] page = PrescriptionsPage(Repository(rows), {"*"}, SimpleNamespace(id=7, name="测试医生")) page.resize(1200, 700) page.show() page.refresh() page.table.sortItems(1, Qt.SortOrder.AscendingOrder) page.table.setColumnWidth(1, 190) for _ in range(5): application.processEvents() assert page.table.item(0, 1).text() == "RX-A" assert page.table.item(1, 1).text() == "RX-Z" assert page.table.item(1, 1).data(Qt.ItemDataRole.UserRole)["id"] == 41 assert "请开方,当前处方药材为空白" in page.table.item(1, 1).toolTip() assert page.table.rowHeight(1) > page.table.rowHeight(0) page.close() application.processEvents() def test_number_html_is_escaped_but_remains_readable(application: QApplication) -> None: unsafe_number = "RX-NOT HTML" page = PrescriptionsPage( Repository([row(51, unsafe_number, has_order=1, herbs=[])]), {"*"}, SimpleNamespace(id=7, name="测试医生"), ) page.refresh() delegate = page.table.itemDelegateForColumn(1) option = QStyleOptionViewItem() option.initFrom(page.table) index = page.table.model().index(0, 1) plain_text = delegate.document(option, index, page.table.columnWidth(1)).toPlainText() assert unsafe_number in plain_text assert "ID: 51" in plain_text assert "请开方,当前处方药材为空白" in plain_text page.close() application.processEvents()