256 lines
11 KiB
Python
256 lines
11 KiB
Python
"""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.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", "#1769E8", 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 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, "#A4ADBA", size).toImage()
|
|
assert blue.pixmap(size, size, QIcon.Mode.Disabled).toImage() != original_disabled
|
|
|
|
|
|
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() == []
|