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

642 lines
25 KiB
Python

"""Order contracts at risk when filters, summary and table receive the blue layout."""
from __future__ import annotations
import os
import socket
from copy import deepcopy
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QDate, QPoint, QRect, Qt, QTimer
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QLabel
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui.pages import patients as patients_module
from doctor_workstation.ui.pages.patients import PatientOrdersWorkspace, PatientsPage
from doctor_workstation.ui.theme import apply_theme
class _Repository:
def __init__(self) -> None:
self.queries: list[dict[str, Any]] = []
self.rows = [
{
"id": 901 + index,
"order_no": f"PO2026081000{index + 1}",
"patient_name": name,
"patient_phone_masked": f"186****482{index}",
"recipient_phone": f"1860000482{index}",
"prescription_id": 801 + index,
"diagnosis_id": 501 + index,
"amount": 368 + index,
"effective_amount": 368 + index,
"prescription_audit_status": 1,
"payment_slip_audit_status": 1,
"fulfillment_status": 2,
"assistant_name": "周医助",
"doctor_name": "陈医生(演示)",
"creator_name": "周医助",
# Source-only values must not fill display fields that are absent.
"create_time": "2026-09-05 10:15:00",
"pay_orders": [{"id": 2001 + index, "pay_amount": 368 + index}],
}
for index, name in enumerate(("阿青", "林青", "赵青"))
]
self.total = 47
self.summary = {
"orders": 47,
"amount": 12368.5,
"pending": 8,
"completed": 9,
"rejected": 2,
"rejection_rate": 4.3,
}
def patient_orders(self, **query: Any) -> dict[str, Any]:
self.queries.append(query)
return {
"lists": deepcopy(self.rows),
"count": self.total,
"extend": {
"scope": {"label": "测试部门订单范围"},
"summary": deepcopy(self.summary),
},
}
def list_patients(self, **_query: Any) -> dict[str, Any]:
return {
"lists": [{"id": 501, "diagnosis_id": 501, "patient_name": "阿青"}],
"count": 1,
"extend": {"scope": {"label": "测试患者范围"}},
}
def patient_progress(self, **_query: Any) -> dict[str, Any]:
return {
"lists": [],
"count": 0,
"extend": {"scope": {"label": "测试面诊范围"}},
}
def _settle(application: QApplication) -> None:
for _ in range(3):
application.processEvents()
def _click(widget, application: QApplication) -> None:
QTest.mouseClick(widget, Qt.MouseButton.LeftButton)
_settle(application)
def _rect_in(widget, parent) -> QRect:
return QRect(widget.mapTo(parent, QPoint()), widget.size())
def _banner_text(workspace: PatientOrdersWorkspace) -> str:
return " ".join(label.text() for label in workspace.banner.findChildren(QLabel))
@pytest.fixture(scope="module")
def application() -> QApplication:
application = QApplication.instance() or QApplication([])
apply_theme(application)
return application
@pytest.fixture
def workspace_factory(application: QApplication, monkeypatch: pytest.MonkeyPatch):
def immediate(function: Any, *, on_success=None, on_error=None, on_finished=None):
try:
result = function()
except Exception as error:
if on_error:
on_error(error)
raise
else:
if on_success:
on_success(result)
finally:
if on_finished:
on_finished()
def reject_network(*_args: Any, **_kwargs: Any):
pytest.fail("Order visual tests must use only local fixture data")
monkeypatch.setattr(socket.socket, "connect", reject_network)
monkeypatch.setattr(socket.socket, "connect_ex", reject_network)
monkeypatch.setattr(socket, "create_connection", reject_network)
monkeypatch.setattr(patients_module, "run_async", immediate)
opened = []
def create(*, permissions=("*",), width=1270, height=680, page=False):
repository = _Repository()
widget = (
PatientsPage(repository, permissions=PermissionSet(list(permissions)))
if page
else PatientOrdersWorkspace(repository, PermissionSet(list(permissions)))
)
opened.append(widget)
widget.resize(width, height)
widget.show()
if not page:
widget.refresh()
_settle(application)
return widget, repository
yield create
for widget in opened:
for timer in widget.findChildren(QTimer):
timer.stop()
widget.close()
widget.deleteLater()
_settle(application)
def test_filter_defaults_and_reset_keep_edited_dates_but_remove_query_limit(
application: QApplication, workspace_factory
) -> None:
workspace, repository = workspace_factory()
default_query = {
"keyword": "",
"prescription_audit_status": None,
"payment_slip_audit_status": None,
"fulfillment_status": None,
"start_date": "",
"end_date": "",
"page_no": 1,
"page_size": 15,
}
assert repository.queries[0] == default_query
assert all(query == {**default_query, "page_no": index + 1}
for index, query in enumerate(repository.queries))
assert not workspace.use_dates.isChecked()
assert not workspace.start_date.isEnabled() and not workspace.end_date.isEnabled()
assert workspace.start_date.date() == QDate.currentDate().addDays(-30)
assert workspace.end_date.date() == QDate.currentDate()
assert all(
combo.currentData() is None
for combo in (workspace.rx_audit, workspace.pay_audit, workspace.fulfillment)
)
before_filters = len(repository.queries)
workspace.keyword_edit.setText(" 林医生 ")
workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2))
workspace.pay_audit.setCurrentIndex(workspace.pay_audit.findData(0))
workspace.fulfillment.setCurrentIndex(workspace.fulfillment.findData(9))
assert len(repository.queries) == before_filters # Changing status does not auto-submit.
_click(workspace.use_dates, application)
assert workspace.start_date.isEnabled() and workspace.end_date.isEnabled()
start, end = QDate(2026, 7, 3), QDate(2026, 8, 8)
workspace.start_date.setDate(start)
workspace.end_date.setDate(end)
_click(workspace.search_button, application)
assert repository.queries[before_filters] == {
**default_query,
"keyword": "林医生",
"prescription_audit_status": 2,
"payment_slip_audit_status": 0,
"fulfillment_status": 9,
"start_date": "2026-07-03",
"end_date": "2026-08-08",
}
workspace.pager.load_more()
assert repository.queries[-1]["page_no"] == 2
assert repository.queries[-1]["page_size"] == 15
workspace.keyword_edit.setText("新检索")
QTest.keyClick(workspace.keyword_edit, Qt.Key.Key_Return)
_settle(application)
assert next(query for query in repository.queries if query["keyword"] == "新检索")["page_no"] == 1
workspace.start_date.setDate(end.addDays(1))
before = len(repository.queries)
_click(workspace.search_button, application)
assert len(repository.queries) == before
assert workspace.banner.isVisible()
assert "开始日期不能晚于结束日期" in _banner_text(workspace)
_click(workspace.reset_button, application)
assert repository.queries[-1] == {**default_query, "page_no": repository.queries[-1]["page_no"]}
assert workspace.keyword_edit.text() == ""
assert not workspace.use_dates.isChecked()
assert not workspace.start_date.isEnabled() and not workspace.end_date.isEnabled()
assert workspace.start_date.date() == end.addDays(1)
assert workspace.end_date.date() == end
assert not workspace.banner.isVisible()
def test_twelve_columns_keep_source_text_distinct_ids_and_missing_display_fields(
application: QApplication, workspace_factory
) -> None:
workspace, repository = workspace_factory()
row = repository.rows[0]
row["order_no"] = "PO-非常长的订单号-20260905-ABCDEFGHIJ"
row["patient_name"] = "用于检查省略与提示的较长患者姓名"
row["doctor_name"] = "用于检查分行显示与完整提示信息的开方医生(演示)"
workspace.refresh()
_settle(application)
table = workspace.table
assert [table.horizontalHeaderItem(column).text() for column in range(table.columnCount())] == [
"订单",
"患者",
"处方 / 诊单",
"有效金额",
"处方审核",
"支付审核",
"履约",
"支付单",
"归属助理",
"开方人",
"创建人",
"创建时间",
]
index = next(
index
for index in range(table.rowCount())
if table.item(index, 0).data(Qt.ItemDataRole.UserRole)["id"] == 901
)
assert table.item(index, 0).text() == f"{row['order_no']} · #901"
assert table.item(index, 1).text() == f"{row['patient_name']} · 186****4820"
assert table.item(index, 2).text() == "#801 / #501"
assert table.item(index, 9).text() == row["doctor_name"]
assert table.item(index, 0).toolTip() == table.item(index, 0).text()
assert table.item(index, 1).toolTip() == table.item(index, 1).text()
assert table.item(index, 9).toolTip() == row["doctor_name"]
assert table.item(index, 7).text() == "—"
assert table.item(index, 11).text() == "—"
for column in range(12):
assert not table.isColumnHidden(column)
assert table.item(index, column).data(Qt.ItemDataRole.UserRole) == row
assert row["recipient_phone"] not in table.item(index, column).text()
def test_sorted_selection_buttons_and_menu_emit_current_order_resource_ids(
application: QApplication, workspace_factory
) -> None:
workspace, _repository = workspace_factory()
table = workspace.table
emitted = []
workspace.diagnosis_requested.connect(lambda row: emitted.append(("diagnosis", row)))
workspace.detail_requested.connect(lambda row: emitted.append(("detail", row)))
workspace.action_requested.connect(lambda key, row: emitted.append((key, row)))
table.sortItems(0, Qt.SortOrder.AscendingOrder)
first_id = table.item(0, 0).data(Qt.ItemDataRole.UserRole)["id"]
table.sortItems(0, Qt.SortOrder.DescendingOrder)
_settle(application)
assert table.item(0, 0).data(Qt.ItemDataRole.UserRole)["id"] != first_id
for index in (0, 2, 1):
table.selectRow(index)
_settle(application)
expected = table.item(index, 0).data(Qt.ItemDataRole.UserRole)
workspace.diagnosis_button.click()
workspace.detail_button.click()
next(
action for action in workspace.action_menu.actions() if action.text() == "确认发货"
).trigger()
assert [kind for kind, _row in emitted[-3:]] == ["diagnosis", "detail", "ship"]
for _kind, row in emitted[-3:]:
assert (row["id"], row["prescription_id"], row["diagnosis_id"]) == (
expected["id"],
expected["prescription_id"],
expected["diagnosis_id"],
)
@pytest.mark.parametrize(
("permissions", "menu", "detail"),
[
(("*",), ["撤回支付审核", "修改快递单号", "确认发货", "上传药房"], True),
(("tcm.prescriptionOrder/detail",), [], True),
(("tcm.prescriptionOrder/ship",), ["确认发货"], False),
(("tcm.prescriptionOrder.detail", "tcm.prescriptionOrder.ship"), [], False),
((), [], False),
],
)
def test_selected_order_menu_requires_canonical_permissions(
application: QApplication, workspace_factory, permissions, menu, detail
) -> None:
workspace, _repository = workspace_factory(permissions=permissions)
assert [action.text() for action in workspace.action_menu.actions()] == menu
assert workspace.action_button.isVisible() is bool(menu)
assert workspace.detail_button.isVisible() is detail
assert workspace.diagnosis_button.isEnabled()
emitted = []
workspace.detail_requested.connect(lambda row: emitted.append(row["id"]))
workspace._request_detail() # The double-click path also checks permission.
assert bool(emitted) is detail
workspace.permissions = PermissionSet([])
workspace._selection_changed()
_settle(application)
assert workspace.action_menu.actions() == []
assert not workspace.action_button.isVisible()
assert not workspace.detail_button.isVisible()
@pytest.mark.parametrize(
("rx", "pay", "fulfillment", "rx_text", "pay_text", "state", "exclusion"),
[
(0, 1, 4, "待审核", "已通过", "已取消", "已取消不计入"),
(1, 2, 9, "已通过", "已驳回", "拒收", "拒收不计入"),
(2, 0, 10, "已驳回", "待审核", "退款", "退款不计入"),
(1, 1, 6, "已通过", "已通过", "已签收", ""),
],
)
def test_amount_exclusion_reasons_and_independent_status_text_survive_rendering(
application: QApplication,
workspace_factory,
rx,
pay,
fulfillment,
rx_text,
pay_text,
state,
exclusion,
) -> None:
workspace, repository = workspace_factory()
repository.rows = [
{
**repository.rows[0],
"prescription_audit_status": rx,
"payment_slip_audit_status": pay,
"fulfillment_status": fulfillment,
"amount_included": not bool(exclusion),
"amount_exclusion_text": exclusion,
"effective_amount": 12368.5,
}
]
workspace.refresh()
_settle(application)
assert workspace.table.item(0, 3).text() == (exclusion or "¥12,368.50")
assert [workspace.table.item(0, column).text() for column in (4, 5, 6)] == [
rx_text,
pay_text,
state,
]
if fulfillment == 6:
refund = next(
action for action in workspace.action_menu.actions() if action.text() == "退款"
)
assert refund.property("danger") is True
def test_summary_uses_response_scope_and_legacy_aliases_without_recomputing_rows(
application: QApplication, workspace_factory
) -> None:
workspace, repository = workspace_factory()
assert {key: label.text() for key, label in workspace.metrics.items()} == {
"orders": "47",
"amount": "¥12,368.50",
"pending": "8",
"completed": "9",
"rejected": "2",
"rejection_rate": "4.3%",
}
assert workspace.scope_label.text() == "测试部门订单范围"
assert workspace.pager.total == 47
assert workspace.pager.page_size == 15
assert workspace.pager.height() == 24
# Preserve the existing ratio compatibility, including its known ambiguity.
repository.summary = {
"order_count": 20,
"effective_amount": 700.25,
"pending_audit": 3,
"completed": 4,
"rejected": 1,
"rejection_rate": 0.05,
}
workspace.refresh()
_settle(application)
assert {key: label.text() for key, label in workspace.metrics.items()} == {
"orders": "20",
"amount": "¥700.25",
"pending": "3",
"completed": "4",
"rejected": "1",
"rejection_rate": "5.0%",
}
before = len(repository.queries)
for metric in workspace.metrics.values():
_click(metric, application)
assert len(repository.queries) == before # The six metrics are read-only.
@pytest.mark.parametrize("silent", [False, True])
def test_loading_freezes_queries_resets_rows_and_ignores_stale_success_and_failure(
application: QApplication, workspace_factory, monkeypatch: pytest.MonkeyPatch, silent: bool
) -> None:
workspace, repository = workspace_factory()
table = workspace.table
queued = []
monkeypatch.setattr(
patients_module,
"run_async",
lambda function, **callbacks: queued.append((function, callbacks)),
)
workspace.keyword_edit.setText("旧请求")
workspace.refresh(silent=silent)
workspace.keyword_edit.setText("最新请求")
workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2))
workspace.pay_audit.setCurrentIndex(workspace.pay_audit.findData(0))
workspace.fulfillment.setCurrentIndex(workspace.fulfillment.findData(9))
workspace.use_dates.setChecked(True)
workspace.start_date.setDate(QDate(2026, 7, 1))
workspace.end_date.setDate(QDate(2026, 8, 1))
workspace.refresh(silent=silent)
_settle(application)
assert table.rowCount() == 0
assert workspace.content_stack.currentIndex() == 1
assert workspace.pager.isVisible()
assert not workspace.banner.isVisible()
workspace.keyword_edit.setText("尚未提交")
workspace.rx_audit.setCurrentIndex(0)
workspace.use_dates.setChecked(False)
newer = queued[1][0]()
assert repository.queries[-1] == {
"keyword": "最新请求",
"prescription_audit_status": 2,
"payment_slip_audit_status": 0,
"fulfillment_status": 9,
"start_date": "2026-07-01",
"end_date": "2026-08-01",
"page_no": 1,
"page_size": 15,
}
newer["extend"]["scope"]["label"] = "最新范围"
queued[1][1]["on_success"](newer)
current_item = table.item(0, 0)
stale = queued[0][0]()
assert repository.queries[-1]["keyword"] == "旧请求"
queued[0][1]["on_success"](stale)
queued[0][1]["on_error"](RuntimeError("过期失败"))
_settle(application)
assert table.item(0, 0) is current_item
assert workspace.scope_label.text() == "最新范围"
assert not workspace.banner.isVisible()
workspace.keyword_edit.setText("最新请求")
workspace.rx_audit.setCurrentIndex(workspace.rx_audit.findData(2))
workspace.use_dates.setChecked(True)
workspace.refresh(silent=silent)
queued[-1][1]["on_error"](RuntimeError("订单查询失败"))
_settle(application)
assert table.item(0, 0) is current_item
assert workspace.metrics["orders"].text() == "47"
assert workspace.banner.isVisible()
assert "订单查询失败" in _banner_text(workspace)
workspace.refresh(silent=silent)
queued[-1][1]["on_success"]({"lists": [], "count": 0})
_settle(application)
assert table.rowCount() == 0
assert workspace.content_stack.currentIndex() == 1
assert "当前范围内暂无订单" in " ".join(
label.text() for label in workspace.content_stack.currentWidget().findChildren(QLabel)
)
assert not workspace.diagnosis_button.isEnabled()
assert not workspace.detail_button.isEnabled()
assert workspace.action_menu.actions() == []
assert workspace.pager.total == 0
assert workspace.metrics["orders"].text() == "0"
assert workspace.metrics["amount"].text() == "¥0.00"
assert not workspace.banner.isVisible()
@pytest.mark.parametrize(("width", "height"), [(1270, 680), (1014, 490), (760, 380)])
def test_layout_keeps_filters_actions_and_load_status_reachable_at_narrow_viewports(
application: QApplication, workspace_factory, width: int, height: int
) -> None:
workspace, _repository = workspace_factory(width=width, height=height)
table = workspace.table
assert workspace.size().width() == width
assert workspace.size().height() == height
assert table.columnCount() == 12
assert table.horizontalHeader().height() == 46
assert all(table.rowHeight(index) == 68 for index in range(table.rowCount()))
assert table.font().pixelSize() == 14
assert workspace.scope_label.font().pixelSize() == 13
assert all(metric.font().pixelSize() == 18 for metric in workspace.metrics.values())
for widget in (
workspace.keyword_edit,
workspace.rx_audit,
workspace.pay_audit,
workspace.fulfillment,
workspace.search_button,
workspace.reset_button,
workspace.use_dates,
workspace.start_date,
workspace.end_date,
):
assert widget.isVisibleTo(workspace)
assert workspace.filter_card.rect().contains(_rect_in(widget, workspace.filter_card))
for metric in workspace.metrics.values():
assert workspace.summary_strip.rect().contains(_rect_in(metric, workspace.summary_strip))
if width == 1270:
assert workspace.filter_card.height() == 132
assert workspace.summary_strip.height() == 96
if width == 760:
assert workspace.filter_card.height() == 184
assert (
len(
{
metric.mapTo(workspace.summary_strip, QPoint()).y()
for metric in workspace.metrics.values()
}
)
== 2
)
assert table.horizontalScrollBar().maximum() > 0
assert workspace.scroll.verticalScrollBar().maximum() > 0
for host in (
workspace.filter_card,
workspace.summary_strip,
workspace.action_bar,
workspace.pager,
):
workspace.scroll.ensureWidgetVisible(host, 0, 0)
_settle(application)
assert (
workspace.scroll.viewport().rect().contains(_rect_in(host, workspace.scroll.viewport()))
), host.objectName()
for button in (workspace.diagnosis_button, workspace.detail_button, workspace.action_button):
assert workspace.action_bar.rect().contains(_rect_in(button, workspace.action_bar))
assert workspace.pager.height() == 24
for widget in (workspace.pager.summary_label,):
assert workspace.pager.rect().contains(_rect_in(widget, workspace.pager))
table.horizontalScrollBar().setValue(table.horizontalScrollBar().maximum())
table.scrollToBottom()
_settle(application)
assert table.columnViewportPosition(11) >= 0
assert table.columnViewportPosition(11) + table.columnWidth(11) <= table.viewport().width()
assert table.rowViewportPosition(2) + table.rowHeight(2) <= table.viewport().height()
def test_tab_switches_keep_patient_styles_scope_deduplication_and_progress_timer(
application: QApplication, workspace_factory
) -> None:
page, _repository = workspace_factory(width=1328, height=884, page=True)
patient = page.patient_workspace
progress = page.progress_workspace
patient_style = patient.styleSheet()
assert patient.table.objectName() == "PatientTable"
assert patient.table.columnCount() == 10
assert patient.table.rowHeight(0) == 66
assert patient.table.horizontalHeader().height() == 42
assert patient.table.font().pixelSize() == 14
assert not page.scope_badge.isVisible()
assert not patient.search_toolbar.isVisible()
page.filter_disclosure.set_expanded(True)
_settle(application)
assert patient.search_toolbar.isVisible()
assert not progress.timer.isActive()
page.tabs.setCurrentIndex(2)
_settle(application)
progress_style = progress.styleSheet()
progress_table_font = progress.schedule_table.font()
progress_metric_font = progress.overview["total"][0].font()
assert progress.timer.interval() == 15_000
assert progress.timer.isActive()
assert not page.scope_badge.isVisible()
assert not progress.scope_label.isVisible()
page.filter_disclosure.set_expanded(True)
_settle(application)
assert progress.scope_label.isVisible()
assert progress_table_font.pixelSize() == 14
assert progress_metric_font.pixelSize() == 18
assert progress.schedule_table.rowCount() == 7
assert progress.schedule_table.columnCount() == 7
page.tabs.setCurrentIndex(1)
_settle(application)
assert not progress.timer.isActive()
assert not page.scope_badge.isVisible()
assert not patient.search_toolbar.isVisible()
assert page.order_workspace.scope_label.isVisible()
assert page.order_workspace.scope_label.text() == "测试部门订单范围"
assert patient.styleSheet() == patient_style
assert progress.styleSheet() == progress_style
assert progress.schedule_table.font() == progress_table_font
assert progress.overview["total"][0].font() == progress_metric_font
assert patient.table.rowHeight(0) == 66
assert patient.table.horizontalHeader().height() == 42
page.tabs.setCurrentIndex(0)
_settle(application)
assert patient.search_toolbar.isVisible()
assert not page.scope_badge.isVisible()
assert not progress.timer.isActive()
page.tabs.setCurrentIndex(2)
_settle(application)
assert not page.scope_badge.isVisible()
assert progress.scope_label.isVisible()
assert progress.timer.isActive()
page.hide()
_settle(application)
assert not progress.timer.isActive()
page.show()
_settle(application)
assert progress.timer.isActive()