Files
zyt/app/tests/test_appointments_parity_ui.py
T
2026-09-09 15:47:48 +08:00

912 lines
30 KiB
Python

"""Parity contracts for the admin appointment list port."""
from __future__ import annotations
import os
from copy import deepcopy
from types import SimpleNamespace
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QElapsedTimer
from PySide6.QtTest import QTest
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QDialog,
QDialogButtonBox,
QLabel,
QPushButton,
)
from doctor_workstation.core.errors import ApiProtocolError
from doctor_workstation.core.models import Appointment, PageResult
from doctor_workstation.core.permissions import PermissionSet
from doctor_workstation.services.mock_repository import DemoDoctorRepository
from doctor_workstation.services.repository import RemoteDoctorRepository
from doctor_workstation.ui.pages import appointments as appointments_module
from doctor_workstation.ui.pages.appointments import (
AppointmentsPage,
_diagnosis_id,
_video_patient_id,
prescription_action_label,
)
from doctor_workstation.ui.shell import _match_navigation
class RecordingClient:
def __init__(self) -> None:
self.get_calls: list[tuple[str, dict[str, Any]]] = []
self.post_calls: list[tuple[str, dict[str, Any]]] = []
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
self.get_calls.append((endpoint, dict(params or {})))
if endpoint == "doctor.appointment/detail":
return {"id": int((params or {}).get("id", 0)), "patient_name": "测试"}
if endpoint == "dept.dept/all":
return [{"id": 10, "name": "中医门诊", "children": []}]
return {"lists": [], "count": 0, "extend": {"status_count": {"1": 2}}}
def post(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
self.post_calls.append((endpoint, dict(payload or {})))
if endpoint == "tcm.diagnosis/generateMiniProgramQrcode":
return {"qrcode_url": "https://example.test/uploads/video-qr.png"}
return {"ok": True}
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
def test_remote_appointment_list_and_detail_hit_admin_endpoints() -> None:
client = RecordingClient()
repo = RemoteDoctorRepository(client) # type: ignore[arg-type]
repo.list_appointments(
page_no=1,
page_size=15,
status=1,
start_date="2026-08-11",
end_date="2026-08-11",
include_status_counts=1,
diagnosis_confirmed="1",
assistant_dept_id=10,
patient_name="林",
)
detail = repo.get_appointment_detail(101)
departments = repo.list_departments()
assert client.get_calls[0][0] == "doctor.appointment/lists"
assert client.get_calls[0][1]["include_status_counts"] == 1
assert client.get_calls[0][1]["assistant_dept_id"] == 10
assert "diag_scope_relax" not in client.get_calls[0][1]
assert client.get_calls[1] == ("doctor.appointment/detail", {"id": 101})
assert client.get_calls[2] == ("dept.dept/all", {})
assert detail["id"] == 101
assert departments[0]["id"] == 10
def test_unlimited_date_relaxes_today_scope_for_pending_status() -> None:
client = RecordingClient()
repo = RemoteDoctorRepository(client) # type: ignore[arg-type]
repo.list_appointments(status=1, diag_scope_relax=1, page_no=1, page_size=15)
params = client.get_calls[0][1]
assert "start_date" not in params
assert "end_date" not in params
assert "diag_scope_relax" not in params
def test_video_qr_uses_doctor_id_without_fake_diagnosis_id() -> None:
client = RecordingClient()
repo = RemoteDoctorRepository(client) # type: ignore[arg-type]
result = repo.generate_video_qrcode(
diagnosis_id=501,
doctor_id=88,
patient_id=301,
share_user_id=9,
)
endpoint, payload = client.post_calls[-1]
assert endpoint == "tcm.diagnosis/generateMiniProgramQrcode"
assert payload == {
"diagnosis_id": 501,
"doctor_id": 88,
"patient_id": 301,
"share_user_id": 9,
"mini_program_path": "pages/login/login",
}
assert result["qrcode_url"].endswith("video-qr.png")
def test_legacy_empty_string_prescription_response_is_scoped_to_not_found() -> None:
class LegacyClient(RecordingClient):
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
if endpoint == "tcm.prescription/getByAppointment":
raise ApiProtocolError("envelope must be an object", data="")
return super().get(endpoint, params)
repo = RemoteDoctorRepository(LegacyClient()) # type: ignore[arg-type]
assert repo.get_prescription_by_appointment(101) is None
class BrokenClient(LegacyClient):
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
raise ApiProtocolError("invalid JSON")
broken = RemoteDoctorRepository(BrokenClient()) # type: ignore[arg-type]
with pytest.raises(ApiProtocolError, match="invalid JSON"):
broken.get_prescription_by_appointment(101)
def test_shell_matches_appointment_list_by_route_not_shared_permission() -> None:
row = {
"name": "挂号列表",
"paths": "/appointments",
"component": "tcm/appointment/list",
"perms": "doctor.appointment/lists",
}
item = _match_navigation(row)
assert item is not None
assert item.key == "appointments"
reception = {
"name": "接诊台",
"paths": "/reception",
"component": "patient/reception/index",
"perms": "doctor.appointment/lists",
}
assert _match_navigation(reception).key == "reception" # type: ignore[union-attr]
def test_diagnosis_and_patient_ids_stay_distinct_for_video() -> None:
row = Appointment.from_dict(
{
"id": 101,
"patient_id": 301,
"diagnosis_id": 501,
"source_patient_id": 301,
"status": 1,
"has_prescription": 0,
}
)
assert _diagnosis_id(row) == 501
assert _video_patient_id(row) == 301
assert _video_patient_id({"diagnosis_id": 501, "patient_id": 301}) == 0
assert _video_patient_id({"diagnosis_id": 501, "patient_id": 501}) == 0
assert prescription_action_label(row) == "开方"
approved = Appointment.from_dict(
{
"id": 102,
"prescription_audit_status": 1,
"prescription_void_status": 0,
"has_prescription": 1,
}
)
assert prescription_action_label(approved) == "查看"
pending = Appointment.from_dict(
{
"id": 103,
"prescription_audit_status": 0,
"prescription_void_status": 0,
"has_prescription": 1,
}
)
assert prescription_action_label(pending) == "编辑处方"
historical_only = Appointment.from_dict(
{
"id": 104,
"prescription_audit_status": 1,
"prescription_void_status": 0,
"has_prescription": 1,
"current_has_prescription": 0,
"current_prescription_id": 0,
}
)
assert prescription_action_label(historical_only) == "开方"
def test_appointment_view_intent_never_degrades_to_edit(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
row = {
"id": 101,
"appointment_id": 101,
"diagnosis_id": 501,
"current_has_prescription": 1,
"current_prescription_id": 81,
"prescription_audit_status": 1,
"prescription_void_status": 0,
}
page = AppointmentsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
monkeypatch.setattr(page, "_current_row", lambda: row)
requested: list[tuple[Any, str]] = []
monkeypatch.setattr(
page,
"_begin_prescription_load",
lambda source, *, mode="open": requested.append((source, mode)),
)
page._open_prescription()
assert requested == [(row, "view")]
monkeypatch.setattr(
page,
"_open_existing_prescription_editor",
lambda _existing: pytest.fail("view intent must never open the editor"),
)
page._prescription_loaded(
{
"id": 81,
"appointment_id": 101,
"audit_status": 2,
"void_status": 0,
},
row,
page._prescription_generation,
"view",
)
assert "状态已变化" in page.banner.label.text()
page.close()
application.processEvents()
def test_appointment_im_action_opens_chat_without_an_existing_live_call(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["doctor.appointment/prescription"]),
)
row = {
"id": 101,
"appointment_id": 101,
"diagnosis_id": 501,
"patient_id": 501,
"source_patient_id": 301,
"patient_name": "测试患者",
"status": 1,
}
emitted: list[dict[str, Any]] = []
monkeypatch.setattr(page, "_current_row", lambda: row)
page.video_requested.connect(emitted.append)
page._request_video()
assert len(emitted) == 1
assert emitted[0]["mode"] == "im"
assert emitted[0]["appointment_id"] == 101
assert emitted[0]["diagnosis_id"] == 501
assert emitted[0]["patient_id"] == 301
warnings: list[str] = []
monkeypatch.setattr(
appointments_module,
"show_toast",
lambda _parent, message, _kind: warnings.append(message),
)
row["status"] = 2
page._request_video()
assert len(emitted) == 1
assert warnings == ["已取消的挂号不可进入 IM 问诊。"]
page.close()
application.processEvents()
def test_appointment_pending_prescription_uses_full_edit_contract(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
updated: list[tuple[int, dict[str, Any]]] = []
existing = {
"id": 81,
"diagnosis_id": 501,
"audit_status": 0,
"void_status": 0,
"patient_name": "林晓岚",
}
class Repository:
def update_prescription(
self, prescription: int, changes: dict[str, Any]
) -> dict[str, Any]:
updated.append((prescription, dict(changes)))
return {"id": prescription}
class AcceptedEditor:
def __init__(self, _repository: Any, source: Any, **kwargs: Any) -> None:
assert source is existing
assert kwargs["mode"] == "edit"
def exec(self) -> QDialog.DialogCode:
return QDialog.DialogCode.Accepted
def payload(self) -> dict[str, Any]:
return {"id": 81, "clinical_diagnosis": "脾气虚"}
def run_immediately(function: Any, **callbacks: Any) -> object:
try:
result = function()
except Exception as error:
callbacks["on_error"](error)
else:
callbacks["on_success"](result)
finally:
callbacks["on_finished"]()
return object()
monkeypatch.setattr(appointments_module, "PrescriptionEditorDialog", AcceptedEditor)
monkeypatch.setattr(appointments_module, "run_async", run_immediately)
page = AppointmentsPage(Repository(), permissions=PermissionSet(["*"]))
monkeypatch.setattr(page, "refresh", lambda **_kwargs: None)
page._prescription_loaded(existing, {"id": 101}, page._prescription_generation)
assert updated == [(81, {"id": 81, "clinical_diagnosis": "脾气虚"})]
page.close()
application.processEvents()
def test_appointment_prescription_seed_keeps_admin_observation_fields(
application: QApplication,
) -> None:
page = AppointmentsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
seed = page._prescription_seed(
{"id": 101, "appointment_id": 101, "diagnosis_id": 501},
{
"id": 501,
"patient_name": "林晓岚",
"tongue": "面象哨兵",
"tongue_image": "舌象哨兵",
"pulse": "脉象哨兵",
"pulse_condition": "脉象详情哨兵",
},
)
assert seed["tongue"] == "面象哨兵"
assert seed["tongue_image"] == "舌象哨兵"
assert seed["pulse"] == "脉象哨兵"
assert seed["pulse_condition"] == "脉象详情哨兵"
page.close()
application.processEvents()
def test_appointments_page_default_query_is_today_pending(
application: QApplication,
) -> None:
repo = DemoDoctorRepository()
page = AppointmentsPage(
repo,
permissions=PermissionSet(
[
"doctor.appointment/lists",
"doctor.appointment/complete",
"doctor.appointment/cancel",
"doctor.appointment/prescription",
"doctor.appointment/addDoctorNote",
"tcm.diagnosis/edit",
"tcm.diagnosis/kaifang",
"tcm.diagnosis/videoQr",
]
),
current_user={"id": 1001, "name": "陈医生", "role_id": 1},
)
page.show()
application.processEvents()
page.refresh()
application.processEvents()
completed = QElapsedTimer()
completed.start()
while page.table.rowCount() == 0 and completed.elapsed() < 2_000:
QTest.qWait(10)
filters = page._query_filters()
assert filters["status"] == 1
assert filters["include_status_counts"] == 1
assert filters["start_date"] == filters["end_date"]
assert "diag_scope_relax" not in filters
assert page.table.rowCount() >= 1
page.close()
application.processEvents()
def test_demo_appointment_status_counts_respect_date_scope() -> None:
repo = DemoDoctorRepository()
today = repo._today.isoformat()
result = repo.list_appointments(
page_no=1,
page_size=20,
start_date=today,
end_date=today,
include_status_counts=1,
)
assert isinstance(result, PageResult)
counts = result.extend.get("status_count", {})
assert int(counts.get("1", counts.get(1, 0))) >= 1
assert int(counts.get("3", counts.get(3, 0))) >= 1
def test_appointment_multiline_cells_receive_enough_row_height(
application: QApplication,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["doctor.appointment/lists"]),
current_user={"id": 1001, "role_id": 1},
)
page._loaded(
{
"lists": [
{
"id": 15534,
"patient_name": "张玉英",
"patient_phone": "13800138000",
"gender": 0,
"age": 42,
"height": 162,
"weight": 55,
"doctor_name": "徐国军",
"appointment_date": "2026-08-11",
"appointment_time": "14:30",
"assistant_name": "蒋露露",
"diagnosis_confirmed": 0,
"has_prescription": 0,
"status": 1,
"status_desc": "已预约",
"remark": "—",
}
],
"count": 1,
},
page._generation,
False,
)
patient_text = page.table.item(0, 2).text()
appointment_text = page.table.item(0, 4).text()
assert patient_text == "张玉英"
assert appointment_text.count("\n") == 2
assert "2026-08-11 14:30" in appointment_text
required = 3 * max(16, page.table.fontMetrics().lineSpacing()) + 10
assert page.table.rowHeight(0) >= required
assert page.table.item(0, 4).toolTip() == appointment_text
page.close()
def test_video_qr_dialog_renders_downloaded_image_inside_app(
application: QApplication,
) -> None:
repo = DemoDoctorRepository()
page = AppointmentsPage(
repo,
permissions=PermissionSet(["tcm.diagnosis/videoQr"]),
current_user={"id": 1001, "role_id": 1},
)
url = "https://demo.invalid/qrcode/video/88.png"
dialog = page._build_qr_dialog(
{"patient_name": "鹿立核"},
url,
{"qrcode_url": url, "_image_bytes": repo.download_public_image(url)},
)
image = dialog.findChild(QLabel, "VideoQrImage")
assert image is not None
assert image.pixmap() is not None and not image.pixmap().isNull()
assert dialog.findChild(QLabel, "VideoQrImage").text() == ""
assert dialog.objectName() == "VideoQrDialog"
assert dialog.property("businessDialog") is True
buttons = dialog.findChild(QDialogButtonBox)
assert next(button for button in buttons.buttons() if button.text() == "浏览器打开").property(
"variant"
) == "primary"
dialog.close()
page.close()
application.processEvents()
def test_page_owned_appointment_dialogs_have_stable_visual_contract(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(
["doctor.appointment/complete", "doctor.appointment/addDoctorNote"]
),
current_user={"id": 1001, "role_id": 1},
)
captured: list[QDialog] = []
def reject_dialog(dialog: QDialog) -> QDialog.DialogCode:
captured.append(dialog)
return QDialog.DialogCode.Rejected
monkeypatch.setattr(QDialog, "exec", reject_dialog)
page._open_custom_date()
page._show_detail({"id": 101, "patient_name": "鹿立核"}, page._action_generation)
page.table.set_rows(
[{"id": 101, "diagnosis_id": 501, "patient_id": 301, "status": 1}]
)
page.table.selectRow(0)
page._complete_selected()
assert [dialog.objectName() for dialog in captured] == [
"AppointmentCustomDateDialog",
"AppointmentDetailDialog",
"AppointmentCompleteDialog",
]
assert all(dialog.property("businessDialog") is True for dialog in captured)
assert all(
any(label.property("dialogRole") == "title" for label in dialog.findChildren(QLabel))
for dialog in captured
)
custom_buttons = captured[0].findChild(QDialogButtonBox)
complete_buttons = captured[2].findChild(QDialogButtonBox)
assert custom_buttons.button(QDialogButtonBox.StandardButton.Ok).property(
"variant"
) == "primary"
assert complete_buttons.button(QDialogButtonBox.StandardButton.Ok).property(
"variant"
) == "primary"
for dialog in captured:
dialog.close()
page.close()
application.processEvents()
def test_prescription_case_snapshot_uses_keyword_diagnosis_id(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
requested: list[int] = []
opened: list[tuple[Any, Any]] = []
class Repository:
def get_diagnosis_detail(self, *, diagnosis_id: int) -> dict[str, Any]:
requested.append(diagnosis_id)
return {"id": diagnosis_id, "patient_name": "测试患者"}
def run_immediately(function: Any, **callbacks: Any) -> object:
try:
callbacks["on_success"](function())
except Exception as error: # pragma: no cover - assertion output is clearer
callbacks["on_error"](error)
finally:
callbacks["on_finished"]()
return object()
monkeypatch.setattr(appointments_module, "run_async", run_immediately)
page = AppointmentsPage(
Repository(),
permissions=PermissionSet(["tcm.diagnosis/kaifang"]),
current_user={"id": 9, "role_id": 1},
)
monkeypatch.setattr(
page,
"_open_prescription_editor",
lambda record, detail: opened.append((record, detail)),
)
page._begin_case_record_load(
{"id": 101, "appointment_id": 101, "diagnosis_id": 501, "patient_id": 501}
)
assert requested == [501]
assert opened and opened[0][1]["id"] == 501
page.close()
application.processEvents()
def test_appointment_ai_report_button_visible_with_reception_permission(
application: QApplication,
) -> None:
hidden = AppointmentsPage(
SimpleNamespace(),
permissions=PermissionSet(["tcm.diagnosis/kaifang"]),
)
assert hidden.ai_button.isHidden()
hidden.close()
page = AppointmentsPage(
SimpleNamespace(),
permissions=PermissionSet(["doctor.appointment/reception"]),
)
assert not page.ai_button.isHidden()
assert not page.ai_button.isEnabled()
page.close()
application.processEvents()
def test_appointments_use_full_width_table_with_per_row_im_consult(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(appointments_module, "run_async", lambda *_args, **_kwargs: object())
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(
["doctor.appointment/lists", "doctor.appointment/prescription"]
),
current_user={"id": 1001, "role_id": 1},
)
page.resize(1460, 820)
page.show()
page._apply_responsive_layout()
page._loaded(
{
"lists": [
{
"id": 101,
"diagnosis_id": 501,
"source_patient_id": 301,
"patient_name": "赵俊霞",
"gender": 0,
"age": 53,
"assistant_name": "自媒体4",
"appointment_date": "2026-08-13",
"appointment_time": "09:50",
"status": 1,
"status_desc": "已挂号",
"video_call_hint": {"state": "live", "label": "视频通话进行中"},
}
],
"count": 1,
},
page._generation,
False,
)
application.processEvents()
assert page.content_layout.count() == 1
assert not hasattr(page, "video_panel")
assert not hasattr(page, "video_list")
assert page.table_card.width() == page.content_host.width()
assert page.table.objectName() == "AppointmentTable"
assert (
page.table.verticalScrollMode()
== QAbstractItemView.ScrollMode.ScrollPerPixel
)
assert page.table.horizontalHeaderItem(10).text() == "IM 问诊"
im_host = page.table.cellWidget(0, 10)
assert im_host is not None
im_button = im_host.findChild(QPushButton, "AppointmentImConsultButton")
assert im_button is not None
assert im_button.text() == "IM 问诊"
assert im_button.isEnabled()
assert im_button.accessibleName() == "与赵俊霞进行 IM 问诊"
emitted: list[dict[str, Any]] = []
page.video_requested.connect(emitted.append)
im_button.click()
assert emitted and emitted[0]["mode"] == "im"
assert emitted[0]["appointment_id"] == 101
assert page.date_buttons["today"].isChecked()
page.resize(1024, 640)
page._apply_responsive_layout()
application.processEvents()
assert page.table_card.width() == page.content_host.width()
assert not page.date_overflow_button.isHidden()
assert page.date_buttons["yesterday"].isHidden()
assert not page.date_buttons["today"].isHidden()
page.close()
application.processEvents()
def test_im_entry_does_not_require_a_live_video_hint(
application: QApplication,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["doctor.appointment/prescription"]),
)
rows = [
{
"id": 101,
"diagnosis_id": 501,
"source_patient_id": 301,
"patient_name": "已接通患者",
"status": 1,
"video_call_hint": {"state": "ended", "label": "视频通话已结束"},
},
{
"id": 102,
"diagnosis_id": 502,
"source_patient_id": 302,
"patient_name": "等待患者",
"status": 1,
},
]
page._loaded({"lists": rows, "count": 2}, page._generation, False)
application.processEvents()
buttons = [
page.table.cellWidget(index, 10).findChild(
QPushButton,
"AppointmentImConsultButton",
)
for index in range(page.table.rowCount())
]
buttons_by_name = {
button.accessibleName(): button for button in buttons if button is not None
}
assert set(buttons_by_name) == {
"与已接通患者进行 IM 问诊",
"与等待患者进行 IM 问诊",
}
assert buttons_by_name["与已接通患者进行 IM 问诊"].isEnabled()
waiting = buttons_by_name["与等待患者进行 IM 问诊"]
assert waiting.isEnabled()
assert waiting.toolTip() == "打开患者 IM,可发送消息;通话能力由本次挂号决定"
page.close()
application.processEvents()
def test_im_entry_allows_fulfillable_statuses_and_rejects_terminal_or_unknown_statuses(
application: QApplication,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["doctor.appointment/prescription"]),
)
rows = [
{
"id": index + 100,
"diagnosis_id": index + 500,
"source_patient_id": index + 300,
"patient_name": name,
"status": status,
}
for index, (name, status) in enumerate(
(
("已预约患者", 1),
("已过号患者", 4),
("已取消患者", 2),
("已完成患者", 3),
("未知状态患者", 0),
)
)
]
page._loaded({"lists": rows, "count": len(rows)}, page._generation, False)
application.processEvents()
buttons = {
button.accessibleName(): button
for row_index in range(page.table.rowCount())
if (
button := page.table.cellWidget(row_index, 10).findChild(
QPushButton,
"AppointmentImConsultButton",
)
)
is not None
}
assert buttons["与已预约患者进行 IM 问诊"].isEnabled()
assert buttons["与已过号患者进行 IM 问诊"].isEnabled()
assert not buttons["与已取消患者进行 IM 问诊"].isEnabled()
assert buttons["与已取消患者进行 IM 问诊"].toolTip() == (
"已取消的挂号不可进入 IM 问诊"
)
assert not buttons["与已完成患者进行 IM 问诊"].isEnabled()
assert buttons["与已完成患者进行 IM 问诊"].toolTip() == (
"已完成的挂号不可再进入 IM 问诊"
)
assert not buttons["与未知状态患者进行 IM 问诊"].isEnabled()
assert buttons["与未知状态患者进行 IM 问诊"].toolTip() == (
"当前挂号状态不可进入 IM 问诊"
)
page.close()
application.processEvents()
def test_identical_appointment_poll_keeps_existing_cell_widgets(
application: QApplication,
) -> None:
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["doctor.appointment/lists"]),
current_user={"id": 1001, "role_id": 1},
)
result = {
"lists": [
{
"id": 101,
"diagnosis_id": 501,
"patient_id": 301,
"patient_name": "赵俊霞",
"gender": 2,
"age": 53,
"assistant_name": "周医助",
"appointment_date": "2026-08-17",
"appointment_time": "09:50",
"status": 1,
"status_desc": "已挂号",
}
],
"count": 1,
"extend": {"status_count": {"1": 1}},
}
page._loaded(result, page._generation, True)
selector = page.table.cellWidget(0, 0)
appointment_info = page.table.cellWidget(0, 4)
im_action = page.table.cellWidget(0, 10)
page._loaded(deepcopy(result), page._generation, True)
assert page.table.cellWidget(0, 0) is selector
assert page.table.cellWidget(0, 4) is appointment_info
assert page.table.cellWidget(0, 10) is im_action
changed = deepcopy(result)
changed["lists"][0]["assistant_name"] = "新医助"
page._loaded(changed, page._generation, True)
assert page.table.cellWidget(0, 0) is not selector
assert page.table.cellWidget(0, 4) is not appointment_info
assert page.table.cellWidget(0, 10) is not im_action
page.close()
application.processEvents()
def test_appointments_density_fits_four_rows_in_1366_shell_viewport(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(appointments_module, "run_async", lambda *_args, **_kwargs: object())
page = AppointmentsPage(
DemoDoctorRepository(),
permissions=PermissionSet(["*"]),
current_user={"id": 1001, "role_id": 1},
)
# Approved shared chrome: 208 px rail, 76 px topbar, no outer gutter.
page.resize(1158, 692)
page.show()
application.processEvents()
rows = [
{
"id": 100 + index,
"diagnosis_id": 500 + index,
"patient_id": 300 + index,
"patient_name": f"患者{index}",
"gender": 2,
"age": 40 + index,
"doctor_name": "陈医生",
"assistant_name": "周医助",
"appointment_date": "2026-08-17",
"appointment_time": f"{8 + index:02d}:00",
"status": 1,
"status_desc": "已挂号",
"diagnosis_confirmed": 0,
"has_prescription": 0,
}
for index in range(8)
]
page._loaded(
{"lists": rows, "count": len(rows), "extend": {"status_count": {"1": 8}}},
page._generation,
False,
)
application.processEvents()
heights = [page.table.rowHeight(index) for index in range(page.table.rowCount())]
# Compact title and folded filters leave more room for the patient queue.
assert page.header.height() >= page.header.minimumSizeHint().height()
assert page.header.height() <= 44
assert page.filter_panel.isHidden()
assert all(60 <= height <= 84 for height in heights)
assert all(page.table.cellWidget(row, 4).height() >= page.table.cellWidget(row, 4).minimumSizeHint().height() for row in range(page.table.rowCount()))
assert page.table.viewport().height() // max(heights) >= 4
assert page.pager.isVisibleTo(page)
assert page.content_layout.count() == 1
assert page.table_card.width() == page.content_host.width()
page.resize(1024, 640)
application.processEvents()
assert page.table_card.width() == page.content_host.width()
page.close()
application.processEvents()