860 lines
29 KiB
Python
860 lines
29 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
import pytest
|
|
from PySide6.QtCore import QSize, Qt
|
|
from PySide6.QtWidgets import QApplication, QDialog, QFrame, QToolButton, QWidget
|
|
|
|
from doctor_workstation.ui import shell as shell_module
|
|
from doctor_workstation.ui.shell import NavigationItem, ShellWindow
|
|
from doctor_workstation.ui.theme import COLORS, apply_theme
|
|
|
|
|
|
def _logical_pixel(image: Any, x: int, y: int):
|
|
device_scale = image.devicePixelRatio()
|
|
return image.pixelColor(round(x * device_scale), round(y * device_scale))
|
|
|
|
|
|
class _ShellPageDouble(QWidget):
|
|
def __init__(
|
|
self,
|
|
_repository: Any,
|
|
*,
|
|
permissions: Any,
|
|
current_user: Any,
|
|
parent: QWidget | None = None,
|
|
) -> None:
|
|
super().__init__(parent)
|
|
self.permissions = permissions
|
|
self.current_user = current_user
|
|
self.refresh_count = 0
|
|
self.show_count = 0
|
|
self.ai_context_available = False
|
|
self.ai_open_count = 0
|
|
|
|
def refresh(self) -> None:
|
|
self.refresh_count += 1
|
|
|
|
def open_selected_ai_consult(self) -> bool:
|
|
self.ai_open_count += 1
|
|
return self.ai_context_available
|
|
|
|
def showEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
|
|
super().showEvent(event)
|
|
self.show_count += 1
|
|
self.refresh()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def application() -> QApplication:
|
|
app = QApplication.instance() or QApplication([])
|
|
apply_theme(app)
|
|
return app
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("available_size", "expected_size"),
|
|
[
|
|
(QSize(1920, 1080), QSize(1710, 920)),
|
|
(QSize(1486, 1000), QSize(1486, 920)),
|
|
(QSize(1600, 800), QSize(1600, 800)),
|
|
(QSize(800, 600), QSize(1024, 640)),
|
|
(None, QSize(1710, 920)),
|
|
],
|
|
)
|
|
def test_shell_initial_size_is_bounded_by_logical_available_geometry(
|
|
available_size: QSize | None,
|
|
expected_size: QSize,
|
|
) -> None:
|
|
assert shell_module._bounded_initial_window_size(available_size) == expected_size
|
|
|
|
|
|
def test_patients_navigation_keeps_the_product_menu_title() -> None:
|
|
resolved = shell_module._resolve_navigation(
|
|
[
|
|
{
|
|
"name": "我的患者",
|
|
"component": "first_visit/my_patients",
|
|
"perms": "firstvisit.myPatient/lists",
|
|
}
|
|
],
|
|
{"firstvisit.myPatient/lists"},
|
|
demo_mode=False,
|
|
)
|
|
|
|
assert [(item.key, title) for item, title in resolved] == [("patients", "我的患者")]
|
|
|
|
|
|
def test_appointments_navigation_is_named_reception_and_always_first() -> None:
|
|
resolved = shell_module._resolve_navigation(
|
|
[
|
|
{
|
|
"name": "我的患者",
|
|
"component": "first_visit/my_patients",
|
|
"perms": "firstvisit.myPatient/lists",
|
|
"sort": 99,
|
|
},
|
|
{
|
|
"name": "挂号列表",
|
|
"component": "tcm/appointment/list",
|
|
"perms": "doctor.appointment/lists",
|
|
"sort": 1,
|
|
},
|
|
],
|
|
{"doctor.appointment/lists", "firstvisit.myPatient/lists"},
|
|
demo_mode=False,
|
|
)
|
|
|
|
# 挂号与诊单是两条队列,侧边栏此前两项同名。现在与服务端菜单的“挂号列表”一致。
|
|
assert [(item.key, title) for item, title in resolved] == [
|
|
("appointments", "挂号列表"),
|
|
("patients", "我的患者"),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def shell_window(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> ShellWindow:
|
|
navigation = [
|
|
NavigationItem(key, title, glyph, _ShellPageDouble, (permission,))
|
|
for key, title, glyph, permission in (
|
|
("appointments", "挂号列表", "号", "doctor.appointment/lists"),
|
|
("reception", "接诊台", "◎", "doctor.appointment/lists"),
|
|
(
|
|
"prescription_library",
|
|
"我的处方库",
|
|
"方",
|
|
"tcm.prescriptionLibrary/lists",
|
|
),
|
|
("prescriptions", "已开处方", "笺", "tcm.prescription/lists"),
|
|
("patients", "我的患者", "患", "firstvisit.myPatient/lists"),
|
|
("consultations", "问诊列表", "询", "tcm.diagnosis/lists"),
|
|
("legacy_reference", "原版框架参照", "旧", "legacy.reference/lists"),
|
|
)
|
|
]
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"_resolve_navigation",
|
|
lambda _menu, _permissions, *, demo_mode: [
|
|
(item, item.title) for item in navigation
|
|
],
|
|
)
|
|
window = ShellWindow(
|
|
object(),
|
|
{
|
|
"user": {"name": "陈医生", "department_name": "中医门诊", "role_ids": [1]},
|
|
"demo_mode": True,
|
|
},
|
|
permissions={item.permissions[0] for item in navigation}
|
|
| {"tcm.diagnosis/aiAssistant"},
|
|
)
|
|
window.show()
|
|
application.processEvents()
|
|
yield window
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_legacy_shell_matches_reference_geometry_at_both_acceptance_sizes(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
assert shell_window.navigate("legacy_reference")
|
|
for width, height in ((1024, 640), (1440, 900)):
|
|
shell_window.resize(width, height)
|
|
application.processEvents()
|
|
|
|
# 独立legacy参照路由继续验证原有导轨和外圈留白。
|
|
assert shell_window.sidebar.width() == 190
|
|
assert shell_window.topbar.height() == 62
|
|
assert shell_window.tabs_host.height() == 0
|
|
assert shell_window.workspace.width() == width - 26 - 190
|
|
assert shell_window.stack.width() == width - 26 - 190
|
|
assert shell_window.stack.height() == height - 26 - 62
|
|
assert shell_window.stack.geometry().right() < shell_window.workspace.width()
|
|
assert shell_window.stack.geometry().bottom() < shell_window.workspace.height()
|
|
|
|
image = shell_window.grab().toImage()
|
|
# 侧边栏不再是画布上的一块面板:它就是画布本身,所以导轨内任意一点
|
|
# 都必须与外圈留白同色。原先它是 #F4F7FE→#EEF3FD 的斜向渐变,
|
|
# 沿整条左边缘都对不上画布,形成一道常驻接缝。
|
|
assert _logical_pixel(image, 20, 300).name().lower() == COLORS["canvas"].lower()
|
|
assert _logical_pixel(image, 6, 300).name().lower() == COLORS["canvas"].lower()
|
|
assert _logical_pixel(image, 610, 20).name().lower() == "#ffffff"
|
|
assert _logical_pixel(image, 220, 90).name().lower() == COLORS["canvas_mid"].lower()
|
|
|
|
|
|
def test_legacy_topbar_search_actions_and_navigation_controls_stay_aligned(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
assert shell_window.navigate("legacy_reference")
|
|
for width, height in ((1024, 640), (1366, 768)):
|
|
shell_window.resize(width, height)
|
|
application.processEvents()
|
|
|
|
search_host = shell_window.topbar.findChild(QFrame, "ShellGlobalSearch")
|
|
shortcut_hint = search_host.findChild(QWidget, "ShellShortcutHint")
|
|
assert search_host.size().toTuple() == (265, 36)
|
|
assert shell_window.fold_button.size().toTuple() == (38, 38)
|
|
assert shortcut_hint.size() == shortcut_hint.sizeHint()
|
|
assert (
|
|
abs(search_host.geometry().center().y() - shell_window.fold_button.geometry().center().y())
|
|
<= 1
|
|
)
|
|
assert (
|
|
abs(
|
|
shortcut_hint.mapTo(search_host, shortcut_hint.rect().center()).y()
|
|
- search_host.rect().center().y()
|
|
)
|
|
<= 1
|
|
)
|
|
|
|
shell_window.global_search.setText("患者")
|
|
application.processEvents()
|
|
action_buttons = shell_window.global_search.findChildren(QToolButton)
|
|
assert len(action_buttons) == 2
|
|
for button in action_buttons:
|
|
assert button.size().toTuple() == (22, 18)
|
|
assert shell_window.global_search.rect().contains(button.geometry())
|
|
assert (
|
|
abs(
|
|
button.geometry().center().y()
|
|
- shell_window.global_search.rect().center().y()
|
|
)
|
|
<= 1
|
|
)
|
|
|
|
clear_button = max(action_buttons, key=lambda button: button.x())
|
|
clear_right = clear_button.mapTo(search_host, clear_button.rect().topRight()).x()
|
|
assert clear_right < shortcut_hint.x()
|
|
shell_window.global_search.clear()
|
|
|
|
|
|
def test_registered_pages_are_not_top_level_windows(shell_window: ShellWindow) -> None:
|
|
for page in shell_window.pages.values():
|
|
assert page.parentWidget() is shell_window.stack
|
|
assert not page.isWindow()
|
|
|
|
|
|
def test_reference_shell_has_integrated_search_ai_card_and_window_controls(
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
assert shell_window.navigate("legacy_reference")
|
|
assert shell_window.windowFlags() & Qt.WindowType.FramelessWindowHint
|
|
assert (
|
|
shell_window.global_search.placeholderText() == "搜索患者姓名、手机号、病历号"
|
|
)
|
|
assert shell_window.assistant_card.isVisible()
|
|
assert shell_window.assistant_button.text() == "开始对话"
|
|
assert shell_window.upload_settings_button.text().strip().startswith("设置")
|
|
assert (
|
|
shell_window.upload_settings_button.accessibleName() == "本机录音上传设置"
|
|
)
|
|
assert shell_window.model_label is shell_window.upload_settings_button
|
|
assert shell_window.minimize_button.text() == ""
|
|
assert shell_window.close_button.text() == ""
|
|
|
|
shell_window.set_connection_state(False)
|
|
assert "离线" in shell_window.assistant_status.text()
|
|
shell_window.set_connection_state(True)
|
|
assert "在线" in shell_window.assistant_status.text()
|
|
|
|
|
|
def test_shell_ai_entry_always_opens_patient_picker_even_with_current_selection(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
current = shell_window.pages["appointments"]
|
|
assert isinstance(current, _ShellPageDouble)
|
|
current.ai_context_available = True
|
|
opened: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"select_and_present_ai_consult",
|
|
lambda *args, **kwargs: opened.append((args, kwargs)) or False,
|
|
)
|
|
|
|
shell_window.assistant_button.click()
|
|
application.processEvents()
|
|
|
|
assert current.ai_open_count == 0
|
|
assert len(opened) == 1
|
|
assert opened[0][0][0] is shell_window.repository
|
|
assert opened[0][0][2] is shell_window
|
|
assert shell_window.stack.currentWidget() is current
|
|
|
|
|
|
def test_shell_global_diagnosis_entry_reuses_dialog_and_obeys_permissions(
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
opened: list[tuple[str, int]] = []
|
|
refresh_callbacks: list[Any] = []
|
|
created: list[Any] = []
|
|
|
|
class _SavedSignal:
|
|
def connect(self, callback: Any) -> None:
|
|
refresh_callbacks.append(callback)
|
|
|
|
class _DiagnosisDialogDouble:
|
|
def __init__(
|
|
self,
|
|
repository: Any,
|
|
parent: Any,
|
|
*,
|
|
permissions: Any,
|
|
) -> None:
|
|
self.repository = repository
|
|
self.parent = parent
|
|
self.permissions = permissions
|
|
self.saved = _SavedSignal()
|
|
self.raise_count = 0
|
|
self.activate_count = 0
|
|
created.append(self)
|
|
|
|
def refresh_permissions(self, permissions: Any) -> None:
|
|
self.permissions = permissions
|
|
|
|
def open_for(
|
|
self,
|
|
diagnosis_id: int,
|
|
*,
|
|
editable: bool,
|
|
modeless: bool,
|
|
) -> None:
|
|
assert editable is True
|
|
assert modeless is True
|
|
opened.append(("edit", diagnosis_id))
|
|
|
|
def open_view_only(self, diagnosis_id: int, *, modeless: bool) -> None:
|
|
assert modeless is True
|
|
opened.append(("view", diagnosis_id))
|
|
|
|
def raise_(self) -> None:
|
|
self.raise_count += 1
|
|
|
|
def activateWindow(self) -> None: # noqa: N802 - Qt-compatible test double
|
|
self.activate_count += 1
|
|
|
|
monkeypatch.setattr(shell_module, "DiagnosisDialog", _DiagnosisDialogDouble)
|
|
shell_window._global_diagnosis_dialog = None
|
|
|
|
shell_window.permissions = {"tcm.diagnosis/edit"}
|
|
assert shell_window.open_diagnosis_by_id(501, modeless=True) is created[0]
|
|
shell_window.permissions = {"tcm.diagnosis/readonlyDetail"}
|
|
assert shell_window.open_diagnosis_by_id("502", modeless=True) is created[0]
|
|
shell_window.permissions = {"tcm.diagnosis/*"}
|
|
assert shell_window.open_diagnosis_by_id(503, modeless=True) is created[0]
|
|
|
|
assert opened == [("edit", 501), ("view", 502), ("edit", 503)]
|
|
assert len(created) == 1
|
|
assert created[0].parent is shell_window
|
|
assert len(refresh_callbacks) == 1
|
|
assert created[0].raise_count == 3
|
|
assert created[0].activate_count == 3
|
|
|
|
shell_window.permissions = set()
|
|
assert shell_window.open_diagnosis_by_id(504, modeless=True) is None
|
|
assert shell_window.open_diagnosis_by_id(0, modeless=True) is None
|
|
assert shell_window.open_diagnosis_by_id("invalid", modeless=True) is None
|
|
assert opened == [("edit", 501), ("view", 502), ("edit", 503)]
|
|
|
|
|
|
def test_shell_ai_entry_without_selection_opens_patient_diagnosis_picker(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
appointments = shell_window.pages["appointments"]
|
|
reception = shell_window.pages["reception"]
|
|
assert isinstance(appointments, _ShellPageDouble)
|
|
assert isinstance(reception, _ShellPageDouble)
|
|
opened: list[dict[str, Any]] = []
|
|
|
|
def open_picker(repository: Any, permissions: Any, parent: Any, **kwargs: Any) -> bool:
|
|
opened.append(
|
|
{
|
|
"repository": repository,
|
|
"permissions": permissions,
|
|
"parent": parent,
|
|
**kwargs,
|
|
}
|
|
)
|
|
return False
|
|
|
|
monkeypatch.setattr(shell_module, "select_and_present_ai_consult", open_picker)
|
|
shell_window.global_search.setText("张医生")
|
|
|
|
shell_window.ai_top_button.click()
|
|
application.processEvents()
|
|
|
|
assert appointments.ai_open_count == 0
|
|
assert reception.ai_open_count == 0
|
|
assert shell_window.stack.currentWidget() is appointments
|
|
assert opened == [
|
|
{
|
|
"repository": shell_window.repository,
|
|
"permissions": shell_window.permissions,
|
|
"parent": shell_window,
|
|
"initial_query": "张医生",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("key", ["appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"])
|
|
def test_shell_ai_menu_on_approved_pages_opens_global_patient_picker(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
key: str,
|
|
) -> None:
|
|
page = shell_window.pages[key]
|
|
assert isinstance(page, _ShellPageDouble)
|
|
assert shell_window.navigate(key)
|
|
page.ai_context_available = True
|
|
opened: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"select_and_present_ai_consult",
|
|
lambda *args, **kwargs: opened.append((args, kwargs)) or False,
|
|
)
|
|
|
|
assert shell_window.menu_ai_action.isVisible()
|
|
shell_window.menu_ai_action.trigger()
|
|
application.processEvents()
|
|
|
|
assert page.ai_open_count == 0
|
|
assert len(opened) == 1
|
|
assert shell_window.stack.currentWidget() is page
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "permission"),
|
|
[("appointments", "doctor.appointment/lists"), ("consultations", "tcm.diagnosis/lists"),
|
|
("patients", "firstvisit.myPatient/lists"), ("prescriptions", "tcm.prescription/lists"),
|
|
("prescription_library", "tcm.prescriptionLibrary/lists")],
|
|
)
|
|
def test_shell_hides_global_ai_entries_without_ai_permission(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
key: str,
|
|
permission: str,
|
|
) -> None:
|
|
navigation = [
|
|
NavigationItem(
|
|
key,
|
|
"问诊列表",
|
|
"号",
|
|
_ShellPageDouble,
|
|
(permission,),
|
|
)
|
|
]
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"_resolve_navigation",
|
|
lambda _menu, _permissions, *, demo_mode: [
|
|
(item, item.title) for item in navigation
|
|
],
|
|
)
|
|
window = ShellWindow(
|
|
object(),
|
|
{"user": {"name": "无 AI 权限医生"}, "demo_mode": True},
|
|
permissions={permission},
|
|
)
|
|
window.show()
|
|
application.processEvents()
|
|
|
|
assert window.assistant_card.isHidden()
|
|
assert window.ai_top_button.isHidden()
|
|
assert not window.menu_ai_action.isVisible()
|
|
assert window.menu_sidebar_action.isVisible()
|
|
assert window._active_page_key == key
|
|
assert window.sidebar.width() == 208
|
|
assert window.topbar.height() == 76
|
|
assert window.centralWidget().layout().contentsMargins().isNull()
|
|
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_shell_ai_entry_does_not_require_reception_page(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
navigation = [
|
|
NavigationItem(
|
|
"appointments",
|
|
"问诊列表",
|
|
"号",
|
|
_ShellPageDouble,
|
|
("doctor.appointment/lists",),
|
|
)
|
|
]
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"_resolve_navigation",
|
|
lambda _menu, _permissions, *, demo_mode: [
|
|
(item, item.title) for item in navigation
|
|
],
|
|
)
|
|
opened: list[Any] = []
|
|
monkeypatch.setattr(
|
|
shell_module,
|
|
"select_and_present_ai_consult",
|
|
lambda *args, **kwargs: opened.append((args, kwargs)) or False,
|
|
)
|
|
window = ShellWindow(
|
|
object(),
|
|
{"user": {"name": "无接诊台医生"}, "demo_mode": True},
|
|
permissions={
|
|
"doctor.appointment/lists",
|
|
"tcm.diagnosis/aiAssistant",
|
|
},
|
|
)
|
|
window.show()
|
|
application.processEvents()
|
|
|
|
window.assistant_button.click()
|
|
application.processEvents()
|
|
|
|
assert len(opened) == 1
|
|
assert opened[0][0][0] is window.repository
|
|
assert opened[0][0][2] is window
|
|
assert window.stack.currentWidget() is window.pages["appointments"]
|
|
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_shell_settings_opens_global_local_audio_upload_manager(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def dialog_factory(
|
|
repository: Any,
|
|
diagnosis_id: int | None,
|
|
parent: QWidget,
|
|
) -> QDialog:
|
|
dialog = QDialog(parent)
|
|
dialog.setObjectName("LocalAudioQueueDialog")
|
|
captured.update(
|
|
repository=repository,
|
|
diagnosis_id=diagnosis_id,
|
|
parent=parent,
|
|
dialog=dialog,
|
|
)
|
|
return dialog
|
|
|
|
monkeypatch.setattr(shell_module, "LocalAudioQueueDialog", dialog_factory)
|
|
shell_window.upload_settings_button.click()
|
|
application.processEvents()
|
|
|
|
assert captured["repository"] is shell_window.repository
|
|
assert captured["diagnosis_id"] is None
|
|
assert captured["parent"] is shell_window
|
|
assert captured["dialog"].isVisible()
|
|
|
|
captured["dialog"].reject()
|
|
application.processEvents()
|
|
assert shell_window._local_audio_settings_dialog is None
|
|
|
|
|
|
def test_every_visible_page_navigates_and_visited_tabs_track_active_page(
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
changed: list[str] = []
|
|
shell_window.page_changed.connect(changed.append)
|
|
|
|
for key in (
|
|
"reception",
|
|
"appointments",
|
|
"prescription_library",
|
|
"prescriptions",
|
|
"patients",
|
|
"consultations",
|
|
):
|
|
assert shell_window.navigate(key)
|
|
assert shell_window.stack.currentWidget() is shell_window.pages[key]
|
|
assert shell_window.nav_buttons[key].isChecked()
|
|
assert shell_window.tab_bar.tabData(shell_window.tab_bar.currentIndex()) == key
|
|
|
|
assert shell_window.visited_tab_keys() == (
|
|
"appointments",
|
|
"reception",
|
|
"prescription_library",
|
|
"prescriptions",
|
|
"patients",
|
|
"consultations",
|
|
)
|
|
assert changed[-6:] == [
|
|
"reception",
|
|
"appointments",
|
|
"prescription_library",
|
|
"prescriptions",
|
|
"patients",
|
|
"consultations",
|
|
]
|
|
|
|
|
|
def test_real_navigation_refreshes_once_and_current_page_click_is_a_noop(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
appointments = shell_window.pages["appointments"]
|
|
reception = shell_window.pages["reception"]
|
|
assert isinstance(appointments, _ShellPageDouble)
|
|
assert isinstance(reception, _ShellPageDouble)
|
|
assert appointments.refresh_count == 1
|
|
assert appointments.show_count == 1
|
|
assert reception.refresh_count == 0
|
|
|
|
shell_window.nav_buttons["reception"].click()
|
|
application.processEvents()
|
|
assert reception.refresh_count == 1
|
|
assert reception.show_count == 1
|
|
|
|
shell_window.nav_buttons["reception"].click()
|
|
application.processEvents()
|
|
assert reception.refresh_count == 1
|
|
assert reception.show_count == 1
|
|
|
|
shell_window.nav_buttons["appointments"].click()
|
|
application.processEvents()
|
|
assert appointments.refresh_count == 2
|
|
assert appointments.show_count == 2
|
|
|
|
|
|
def test_non_fixed_tabs_close_and_active_close_renavigates(
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
for key in ("patients", "consultations"):
|
|
assert shell_window.navigate(key)
|
|
|
|
assert not shell_window.close_tab("appointments")
|
|
assert shell_window.close_tab("patients")
|
|
assert "patients" not in shell_window.visited_tab_keys()
|
|
assert (
|
|
shell_window.tab_bar.tabData(shell_window.tab_bar.currentIndex())
|
|
== "consultations"
|
|
)
|
|
|
|
assert shell_window.close_current_tab()
|
|
assert (
|
|
shell_window.tab_bar.tabData(shell_window.tab_bar.currentIndex()) == "appointments"
|
|
)
|
|
assert shell_window.stack.currentWidget() is shell_window.pages["appointments"]
|
|
assert shell_window.nav_buttons["appointments"].isChecked()
|
|
|
|
shell_window.close_all_tabs()
|
|
assert shell_window.visited_tab_keys() == ("appointments",)
|
|
assert shell_window.stack.currentWidget() is shell_window.pages["appointments"]
|
|
|
|
|
|
def test_topbar_refresh_button_reloads_whichever_page_is_open(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
"""The button existed in the code but was never added to the layout.
|
|
|
|
It also matters more than it used to: list loads no longer raise a banner,
|
|
so this is the only control that acknowledges a manual reload.
|
|
"""
|
|
|
|
button = shell_window.refresh_button
|
|
assert button.isVisible()
|
|
assert button.parentWidget() is shell_window.topbar
|
|
|
|
for key in ("consultations", "patients"):
|
|
assert shell_window.navigate(key)
|
|
application.processEvents()
|
|
page = shell_window.pages[key]
|
|
before = page.refresh_count
|
|
button.click()
|
|
application.processEvents()
|
|
assert page.refresh_count == before + 1
|
|
|
|
|
|
def test_topbar_refresh_button_spins_while_the_page_loads(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
"""A spin that cannot stop is worse than no spin at all."""
|
|
|
|
button = shell_window.refresh_button
|
|
resting = button.icon().cacheKey()
|
|
|
|
button.click()
|
|
application.processEvents()
|
|
assert button._timer.isActive()
|
|
|
|
# The page double never reports itself busy, so the spin ends as soon as the
|
|
# minimum has elapsed rather than running for the full cap.
|
|
button._elapsed = _ElapsedStub(button._MIN_MS + 1)
|
|
button._tick()
|
|
|
|
assert not button._timer.isActive()
|
|
assert button.icon().cacheKey() == resting
|
|
|
|
|
|
class _ElapsedStub:
|
|
def __init__(self, value: int) -> None:
|
|
self._value = value
|
|
|
|
def elapsed(self) -> int:
|
|
return self._value
|
|
|
|
def restart(self) -> None:
|
|
return None
|
|
|
|
|
|
def test_legacy_window_ground_carries_the_only_corner(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
"""The bottom-most layer is the one that rounds; nothing nests inside it.
|
|
|
|
The rail used to paint its own 16 px corner on top of a square canvas, so
|
|
each window corner showed a corner inside a corner.
|
|
"""
|
|
|
|
assert shell_window.navigate("legacy_reference")
|
|
application.processEvents()
|
|
image = shell_window.grab().toImage()
|
|
|
|
# Outside the ground's corner there is nothing at all ...
|
|
assert _logical_pixel(image, 2, 2).alpha() == 0
|
|
# ... and well inside it the ground is the flat canvas colour, both in the
|
|
# outer gutter and inside the rail.
|
|
assert _logical_pixel(image, 8, 200).name().lower() == COLORS["canvas"].lower()
|
|
assert _logical_pixel(image, 60, 200).name().lower() == COLORS["canvas"].lower()
|
|
|
|
|
|
def test_approved_pages_share_shell_geometry_and_other_pages_restore(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
"""The six approved pages share chrome; each remaining page keeps its geometry."""
|
|
|
|
geometries = set()
|
|
for key in [*shell_window.pages, "reception", "consultations", "appointments", "patients", "prescriptions", "prescription_library"]:
|
|
assert shell_window.navigate(key)
|
|
application.processEvents()
|
|
geometry = (
|
|
shell_window.sidebar.width(),
|
|
shell_window.workspace.x(),
|
|
shell_window.workspace.width(),
|
|
shell_window.stack.width(),
|
|
)
|
|
if key in {"appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"}:
|
|
assert geometry == (208, 208, shell_window.width() - 208, shell_window.width() - 208)
|
|
assert shell_window.topbar.height() == 76
|
|
assert shell_window.workspace.y() == 0
|
|
else:
|
|
assert geometry == (190, 203, shell_window.width() - 216, shell_window.width() - 216)
|
|
assert shell_window.topbar.height() == 62
|
|
assert shell_window.workspace.y() == 13
|
|
geometries.add(geometry)
|
|
|
|
assert len(geometries) == 1, f"navigation moved the shell: {geometries}"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "expanded_width"),
|
|
[("appointments", 208), ("consultations", 208), ("reception", 208), ("patients", 208), ("prescriptions", 208), ("prescription_library", 208), ("legacy_reference", 190)],
|
|
)
|
|
def test_sidebar_collapse_preserves_active_navigation(
|
|
shell_window: ShellWindow,
|
|
key: str,
|
|
expanded_width: int,
|
|
) -> None:
|
|
assert shell_window.navigate(key)
|
|
title = shell_window.nav_buttons[key].text()
|
|
|
|
shell_window.toggle_sidebar()
|
|
assert shell_window.sidebar.width() == 68
|
|
assert shell_window.nav_buttons[key].text() == ""
|
|
assert shell_window.nav_buttons[key].isChecked()
|
|
|
|
shell_window.toggle_sidebar()
|
|
assert shell_window.sidebar.width() == expanded_width
|
|
assert shell_window.nav_buttons[key].text() == title
|
|
assert shell_window.nav_buttons[key].isChecked()
|
|
|
|
|
|
def test_shell_directional_controls_have_no_unicode_arrow_text(
|
|
shell_window: ShellWindow,
|
|
) -> None:
|
|
assert shell_window.fold_button.text() == ""
|
|
assert shell_window.refresh_button.text() == ""
|
|
assert shell_window.fullscreen_button.text() == ""
|
|
assert shell_window.tabs_menu_button.text() == ""
|
|
assert all(
|
|
arrow not in button.text()
|
|
for button in shell_window.findChildren(QWidget)
|
|
if hasattr(button, "text") and callable(button.text)
|
|
for arrow in ("←", "→", "↑", "↓", "▲", "▼", "▴", "▾")
|
|
)
|
|
|
|
|
|
def test_chat_notification_takes_the_doctor_to_the_matching_workspace(
|
|
application: QApplication,
|
|
shell_window: ShellWindow,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
opened: list[Any] = []
|
|
monkeypatch.setattr(
|
|
ShellWindow,
|
|
"open_diagnosis_by_id",
|
|
lambda self, diagnosis_id, *, modeless=False: opened.append(diagnosis_id),
|
|
)
|
|
center = shell_window.chat_notifications
|
|
assert shell_window.navigate("consultations")
|
|
|
|
center.add_notifications(
|
|
[
|
|
{
|
|
"id": "n1",
|
|
"type": "patient_opened_chat",
|
|
"patient_name": "甘先生",
|
|
"created_at": 1787882294,
|
|
}
|
|
]
|
|
)
|
|
application.processEvents()
|
|
assert [item.id for item in center.pending] == ["n1"]
|
|
|
|
# 患者进入会话 → 直接落到接诊台。
|
|
next(iter(center._cards.values())).open_button.click()
|
|
application.processEvents()
|
|
assert shell_window._active_page_key == "reception"
|
|
assert center.pending == []
|
|
|
|
# 面诊结束 → 打开对应诊单。
|
|
center.add_notifications(
|
|
[
|
|
{
|
|
"id": "n2",
|
|
"type": "consultation_complete",
|
|
"patient_name": "甘先生",
|
|
"doctor_name": "陈医生",
|
|
"diagnosis_id": 8169,
|
|
"created_at": 1787882294,
|
|
}
|
|
]
|
|
)
|
|
next(iter(center._cards.values())).open_button.click()
|
|
application.processEvents()
|
|
assert opened == [8169]
|
|
assert center.pending == []
|