Files
zyt/app/tests/test_prescriptions_tech_blue.py
T
gr d43cb8da36 Mi
erge branch '11-5' of http://git.zhenyangtang.com.cn/ZYT/zyt

it stash pop
2026-11-06 12:02:54 +08:00

351 lines
16 KiB
Python
Executable File

"""Native interactions and reachability unique to the issued-prescription blue UI."""
from __future__ import annotations
import os
import socket
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QPoint, QRect, Qt, QTimer
from PySide6.QtGui import QIcon
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QAbstractItemView, QApplication, QPushButton
from doctor_workstation.core import PermissionSet
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui import icons
from doctor_workstation.ui.design_system import PALETTE
from doctor_workstation.ui.pages import prescriptions as prescriptions_module
from doctor_workstation.ui.shell import ShellWindow
from doctor_workstation.ui.theme import apply_theme
def _settle(application: QApplication) -> None:
for _ in range(5):
application.processEvents()
def _rect_in(widget, parent) -> QRect:
return QRect(widget.mapTo(parent, QPoint()), widget.size())
def _assert_visible_within(widget, viewport) -> None:
assert widget.isVisible()
rect = _rect_in(widget, viewport)
assert viewport.rect().contains(rect), (widget.objectName(), rect, viewport.rect())
def _record_id(table, row: int) -> int:
return int(table.item(row, 0).data(Qt.ItemDataRole.UserRole).id)
@pytest.fixture(scope="module")
def application() -> QApplication:
app = QApplication.instance() or QApplication([])
apply_theme(app)
return app
@pytest.fixture(autouse=True)
def shell_factory(application: QApplication, monkeypatch: pytest.MonkeyPatch):
network_attempts = []
def reject_network(*_args, **_kwargs):
network_attempts.append("socket")
pytest.fail("Prescription blue interaction tests must use local Demo data only")
def immediate(function, *, on_success=None, on_error=None, on_finished=None):
try:
result = function()
except Exception as error:
if on_error is not None:
on_error(error)
raise
else:
if on_success is not None:
on_success(result)
finally:
if on_finished is not None:
on_finished()
monkeypatch.setattr(socket.socket, "connect", reject_network)
monkeypatch.setattr(socket.socket, "connect_ex", reject_network)
monkeypatch.setattr(socket, "create_connection", reject_network)
monkeypatch.setattr(prescriptions_module, "run_async", immediate)
monkeypatch.setenv("DOCTOR_SMOKE_TEST", "1")
opened = []
def create(width=1536, height=960):
repository = DemoDoctorRepository()
session = repository.login(repository.DEMO_ACCOUNT, repository.DEMO_PASSWORD)
permissions = PermissionSet(["*"])
shell = ShellWindow(repository, {
"user": session.user,
"permissions": permissions,
"demo_mode": True,
"menu": [{"perms": "tcm.prescription/lists"}],
}, permissions=permissions)
opened.append(shell)
shell.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
shell.resize(width, height)
shell.show()
assert shell.navigate("prescriptions")
_settle(application)
page = shell.pages["prescriptions"]
assert {_record_id(page.table, row) for row in range(page.table.rowCount())} == {801, 802}
assert (shell.width(), shell.height()) == (width, height)
return shell, page
yield create
for shell in opened:
for timer in shell.findChildren(QTimer):
timer.stop()
shell.close()
shell.deleteLater()
_settle(application)
assert network_attempts == []
@pytest.mark.parametrize(
("width", "height", "filter_height"),
[(1536, 960, 144), (1366, 768, 144), (1024, 768, 200), (1024, 640, 200)],
ids=["full-desktop", "compact-desktop", "small-window", "minimum-window"],
)
def test_shell_preserves_all_columns_filters_and_reachable_pager(
shell_factory, application, width, height, filter_height
) -> None:
shell, page = shell_factory(width, height)
page.filter_disclosure.set_expanded(True)
_settle(application)
table = page.table
assert [column.key for column in table.columns] == [
"__selected__", "sn", "__actions__", "prescription_type", "is_system_auto",
"patient_name", "audit_status", "void_status", "doctor_name", "assistant_name", "create_time",
"__ai_status__", "__ai_agreement__",
]
assert [table.horizontalHeader().logicalIndex(index) for index in range(11)] == [
0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 2,
]
assert table.horizontalHeaderItem(2).text() == "操作"
assert page.filter_card.height() == filter_height
for name in ("quick_date", "start_time", "end_time", "audit_filter", "source_filter",
"sn_filter", "patient_filter", "doctor_filter", "query_button", "reset_button"):
_assert_visible_within(getattr(page, name), page.filter_card)
outer_scroll = page.scroll.verticalScrollBar()
if width >= 1366:
assert outer_scroll.maximum() == 0
_assert_visible_within(page.filter_card, page.scroll.viewport())
_assert_visible_within(page.pager, page.scroll.viewport())
else:
# The compact footer allows 1024x768 to fit without outer scrolling.
if height <= 640:
assert outer_scroll.maximum() > 0
assert table.horizontalScrollBar().maximum() > 0
if width == 1536:
assert table.horizontalScrollBar().maximum() == 0
for row in range(table.rowCount()):
for column in range(11):
assert table.viewport().rect().contains(table.visualItemRect(table.item(row, column))), (row, column)
_assert_visible_within(page.pager, shell)
# Every logical column remains reachable in the native table at every width.
outer_scroll.setValue(outer_scroll.maximum())
for column in range(11):
table.scrollToItem(table.item(0, column), QAbstractItemView.ScrollHint.EnsureVisible)
_settle(application)
assert table.viewport().rect().contains(table.visualItemRect(table.item(0, column))), column
table.horizontalScrollBar().setValue(table.horizontalScrollBar().maximum())
_settle(application)
action_host = table.cellWidget(0, 2)
assert action_host is not None
_assert_visible_within(action_host, table.viewport())
_assert_visible_within(action_host, page.scroll.viewport())
_assert_visible_within(page.pager, page.scroll.viewport())
_assert_visible_within(page.pager.summary_label, shell)
assert page.pager.height() == 24
assert not hasattr(page.pager, "next")
for button in page.pager.findChildren(QPushButton):
if button.isVisible():
_assert_visible_within(button, page.scroll.viewport())
for button in action_host.findChildren(QPushButton):
_assert_visible_within(button, table.viewport())
def test_native_checkbox_center_clicks_and_space_keep_row_binding(shell_factory, application) -> None:
_shell, page = shell_factory()
table = page.table
row = next(index for index in range(table.rowCount()) if _record_id(table, index) == 802)
selector = table.item(row, 0)
table.setCurrentItem(selector)
table.setFocus()
_settle(application)
center = table.visualItemRect(selector).center()
assert table.viewport().rect().contains(center)
assert selector.checkState() == Qt.CheckState.Unchecked
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=center)
assert selector.checkState() == Qt.CheckState.Checked
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=center)
assert selector.checkState() == Qt.CheckState.Unchecked
QTest.keyClick(table, Qt.Key.Key_Space)
assert selector.checkState() == Qt.CheckState.Checked
assert int(table.current_data().id) == 802
assert selector.data(Qt.ItemDataRole.UserRole).id == 802
@pytest.mark.parametrize("order", [Qt.SortOrder.AscendingOrder, Qt.SortOrder.DescendingOrder])
def test_sorted_native_row_view_button_targets_visible_record(
shell_factory, application, monkeypatch, order
) -> None:
_shell, page = shell_factory()
table = page.table
visited = []
monkeypatch.setattr(page, "_view_selected", lambda: visited.append(int(table.current_data().id)))
table.sortItems(1, order)
_settle(application)
expected_order = [801, 802] if order == Qt.SortOrder.AscendingOrder else [802, 801]
assert [_record_id(table, index) for index in range(2)] == expected_order
for row in range(2):
# Start on the other record, so a stale action that only uses selection fails.
table.selectRow(1 - row)
host = table.cellWidget(row, 2)
button = next(button for button in host.findChildren(QPushButton)
if button.accessibleName() == "查看处方")
_assert_visible_within(button, table.viewport())
QTest.mouseClick(button, Qt.MouseButton.LeftButton, pos=button.rect().center())
assert visited[-1] == expected_order[row]
assert int(table.current_data().id) == expected_order[row]
assert visited == expected_order
def test_blue_icon_disabled_variant_does_not_modify_shared_icon_cache(application) -> None:
kind, color, size = "pencil", PALETTE["accent"], 19
shared = icons.icon(kind, color, size)
original_key = shared.cacheKey()
original_normal = shared.pixmap(size, size, QIcon.Mode.Normal).toImage()
original_disabled = shared.pixmap(size, size, QIcon.Mode.Disabled).toImage()
blue = prescriptions_module._blue_prescription_icon(kind, color, size)
assert blue is not shared
assert icons.icon(kind, color, size) is shared
assert shared.cacheKey() == original_key
assert shared.pixmap(size, size, QIcon.Mode.Normal).toImage() == original_normal
assert shared.pixmap(size, size, QIcon.Mode.Disabled).toImage() == original_disabled
assert blue.pixmap(size, size, QIcon.Mode.Normal).toImage() == original_normal
assert blue.pixmap(size, size, QIcon.Mode.Disabled).toImage() == icons.pixmap(kind, PALETTE["disabled"], size).toImage()
assert blue.pixmap(size, size, QIcon.Mode.Disabled).toImage() == original_disabled
# Both surfaces share the disabled colour; changing the returned icon still
# must not mutate the cached icon used by another page.
blue.addPixmap(icons.pixmap(kind, PALETTE["danger"], size), QIcon.Mode.Disabled)
assert shared.cacheKey() == original_key
assert shared.pixmap(size, size, QIcon.Mode.Disabled).toImage() == original_disabled
@pytest.mark.parametrize("column", [1, 3, 4, 5, 6, 7, 8, 9, 10])
def test_all_prescription_data_cells_select_and_copy_without_editing(
shell_factory, application, column,
) -> None:
_shell, page = shell_factory()
table = page.table
item = table.item(0, column)
original = item.text()
row_id = _record_id(table, 0)
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(item).center())
assert table.copy_text.isVisible()
assert _record_id(table, table.currentRow()) == row_id
assert table.cellWidget(0, column) is None
QTest.keyClick(table.copy_text, Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier)
QTest.keyClick(table.copy_text, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
copied = application.clipboard().text()
if column == 1:
assert original in copied and f"ID: {row_id}" in copied
elif column == 5:
assert copied == original.replace(" · ", "\n", 1).replace(" · ", " · ")
elif column == 10:
assert copied == "\n".join(original.rsplit(" ", 1))
else:
assert copied == original
QTest.keyClicks(table.copy_text, "must-not-edit")
assert item.text() == original
assert table.item(0, 0).checkState() == Qt.CheckState.Unchecked
def test_text_drag_copy_and_double_click_do_not_open_prescription(shell_factory, application, monkeypatch):
_shell, page = shell_factory()
table = page.table
opened = []
monkeypatch.setattr(page, "_view_selected", lambda: opened.append(True))
item = table.item(1, 8)
rect = table.visualItemRect(item)
start = QPoint(rect.left() + 12, rect.center().y())
end = QPoint(rect.right() - 12, start.y())
QTest.mousePress(table.viewport(), Qt.MouseButton.LeftButton, pos=start)
QTest.mouseMove(table.viewport(), end, 30)
QTest.mouseRelease(table.viewport(), Qt.MouseButton.LeftButton, pos=end)
assert table.copy_text.textCursor().selectedText() == item.text()
QTest.keyClick(table.copy_text, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
assert application.clipboard().text() == item.text()
table.copy_text.customContextMenuRequested.emit(QPoint(4, 4))
menu = page._ai_context_menu
selected_action = next(action for action in menu.actions() if action.text() == "复制选中文字")
application.clipboard().clear()
selected_action.trigger()
assert application.clipboard().text() == item.text()
menu.close()
QTest.mouseDClick(table.viewport(), Qt.MouseButton.LeftButton, pos=start + QPoint(8, 0))
assert table.copy_text.textCursor().hasSelection()
assert opened == []
def test_copy_context_menu_and_refresh_follow_the_visible_row(shell_factory, application):
_shell, page = shell_factory()
table = page.table
# Copy remains available without AI/report permissions, including blank prescriptions.
page.permissions = PermissionSet([])
table.sortItems(1, Qt.SortOrder.DescendingOrder)
item = table.item(0, 1)
page._open_ai_context_menu(table.visualItemRect(item).center())
menu = page._ai_context_menu
actions = {action.text(): action for action in menu.actions()}
assert set(actions) == {"复制单元格", "复制整行"}
actions["复制单元格"].trigger()
assert item.text() in application.clipboard().text()
assert f"ID: {_record_id(table, 0)}" in application.clipboard().text()
actions["复制整行"].trigger()
copied = application.clipboard().text()
for column in range(3, 11):
assert table.item(0, column).text() in copied
assert "\t" in copied and "复制" not in copied
menu.close()
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(item).center())
assert table.copy_text.isVisible()
table.sortItems(1, Qt.SortOrder.AscendingOrder)
assert table.copy_text.isHidden()
table.setCurrentItem(table.item(0, 5))
QTest.keyClick(table, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
assert application.clipboard().text() == table.item(0, 5).text()
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(table.item(0, 5)).center())
page.refresh()
assert table.copy_text.isHidden()
def test_long_doctor_button_paints_compactly_without_truncating_real_value(shell_factory, application) -> None:
_shell, page = shell_factory(1024, 768)
page.filter_disclosure.set_expanded(True)
_settle(application)
button = page.doctor_filter.button
full_text = "联合会诊专家门诊陈医生、疑难病联合门诊林医生 等3人"
button.setText(full_text)
_settle(application)
assert button.fontMetrics().horizontalAdvance(full_text) > button.width() - 48
assert button.text() == full_text
assert button.toolTip() == full_text
assert not button.grab().isNull() # Execute the actual compact paint path.
assert button.text() == full_text
assert page.doctor_filter.values() == []