Files
zyt/app/tests/test_ai_consult_source_rendering_r36.py
2026-10-27 14:03:35 +08:00

178 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Paint the actual measured source text, including long unbroken record values."""
from __future__ import annotations
import json
import os
from html import escape
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QEvent, Qt, QTimer
from PySide6.QtGui import QTextCursor, QTextDocument
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QLabel, QTextBrowser
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui.dialogs import ai_consult as module
from doctor_workstation.ui.theme import apply_theme
@pytest.fixture(scope="module")
def application():
app = QApplication.instance() or QApplication([])
apply_theme(app)
return app
@pytest.mark.parametrize("rich", [True, False])
def test_long_unbroken_tail_uses_actual_paint_document_and_exact_selection(application, rich):
text = "record_date" * 100 + "TAIL_SENTINEL"
label = module._FittedText(module._reading_html(text) if rich else text, rich=rich)
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
try:
label.show()
for width in (528, 240, 760, 528):
label.resize(width, label.height())
QTest.qWait(5)
label._fit()
view = label._text_view
document = view.document()
assert document is label._measure_document
assert document.toPlainText() == text
layout = document.firstBlock().layout()
assert layout.lineCount() > 5
assert all(layout.lineAt(n).naturalTextWidth() <= view.viewport().width()
for n in range(layout.lineCount()))
cursor = QTextCursor(document)
cursor.movePosition(QTextCursor.MoveOperation.End)
view.setTextCursor(cursor)
assert view.cursorRect().bottom() < view.viewport().height()
assert view.verticalScrollBar().maximum() == 0
image = view.viewport().grab().toImage()
# The final line actually paints at its document position, rather
# than a separate QLabel painter centering one clipped long token.
rect = view.cursorRect()
assert any(image.pixelColor(x, y).alpha() > 0
and image.pixelColor(x, y).red() < 180
for y in range(max(0, rect.top()), min(image.height(), rect.bottom() + 1))
for x in range(image.width()))
label.setSelection(len(text) - len("TAIL_SENTINEL"), len("TAIL_SENTINEL"))
assert label.hasSelectedText() and label.selectedText() == "TAIL_SENTINEL"
finally:
label.close()
label.deleteLater()
@pytest.mark.parametrize("layers", [0, 1, 2])
def test_source_fields_are_readable_without_rewriting_original_reply(application, layers):
record = {"record_date": 1775577600, "record_time": "07:30", "fasting_blood_sugar": 6.2,
"unknown_key": "保留未知字段", "zero": 0, "flag": False, "missing": None,
"nested": {"note": "<script>保留字面文本</script>;血糖 <7.0 mmol/L"}}
raw = json.dumps(record, ensure_ascii=False, separators=(",", ":"))
for _ in range(layers):
raw = escape(raw)
original = raw
rendered = QTextDocument()
rendered.setHtml(module._reading_html(raw))
plain = rendered.toPlainText()
assert "记录日期:2026-04-08" in plain
assert "记录时间:07:30" in plain
assert "6.2" in plain and "unknown_key:保留未知字段" in plain
assert "zero:0" in plain and "flag:否" in plain and "missing:未记录" in plain
assert "<script>保留字面文本</script>" in plain and "血糖 <7.0 mmol/L" in plain
assert "&quot;" not in plain
assert raw == original
@pytest.mark.parametrize("raw", [
'{"record_date":1775577600,"record_date":1775664000,"note":"原文不删"}',
'{"record_date":1775577600,"note":"未闭合尾部',
'{"record_date":1775577600,"number":NaN}',
])
def test_unsupported_source_stays_literal_and_complete(application, raw):
rendered = QTextDocument()
rendered.setHtml(module._reading_html(escape(raw)))
assert rendered.toPlainText() == raw
def test_source_continuation_keeps_date_role_and_one_owning_bullet():
record = escape('{"record_date":1775577600,"note":"原记录终点"}')
raw = ("## 一、原始资料\n- 2026-04-08 · 日常记录原文(非本次建议):\\[\n"
+ record + "\n\\]\n\n## 二、待核对\n- " + "只核对已采集资料,不添加患者事实。" * 5)
original = raw
_, sections, _ = module.parse_structured_report(raw)
assert len(sections[0].items) == 1
assert "日常记录原文(非本次建议)" in sections[0].items[0]
shown = module._report_display_text(sections[0].items[0])
assert "2026-04-08" in shown and "原记录终点" in shown
assert r"\[" not in shown and r"\]" not in shown
assert raw == original
assert "<7.0" in module._plain_markdown("血糖 <7.0,餐后 >10 mmol/L")
def test_pretty_source_is_grouped_before_markdown_changes_keys_or_values():
record = {"record_date": 1775577600, "unknown__key": "字面 **内容** 与 __标记__",
"nested": {"text": "<script>原字面值</script>"}}
raw = ("## 一、原始资料\n- 2026-04-08 · 日常记录原文:\\[\n"
+ json.dumps(record, ensure_ascii=False, indent=2)
+ "\n\\]\n## 二、复核\n- " + "保留资料,不添加患者事实。" * 5)
original = raw
_, sections, _ = module.parse_structured_report(raw)
assert len(sections[0].items) == 1
shown = QTextDocument()
shown.setHtml(module._reading_html(sections[0].items[0]))
assert "unknown__key:字面 **内容** 与 __标记__" in shown.toPlainText()
assert "<script>原字面值</script>" in shown.toPlainText()
assert raw == original
@pytest.mark.parametrize("repeat", [1, 100])
def test_pretty_negative_array_entries_keep_signs_and_do_not_become_bullets(repeat):
values = ",\n".join([" -1", " -0.25", " -3.5e-06"] * repeat)
body = '{\n "record_date":1775577600,\n "values":[\n' + values + '\n ],\n "tail":"NEGATIVE_ARRAY_TAIL"\n}'
raw = ("## 一、资料\n- 日常记录原文:\\[\n" + body
+ "\n\\]\n- 独立下一项\n## 二、核对\n- " + "保留原数据和日期,不编造。" * 5)
_, sections, _ = module.parse_structured_report(raw)
assert len(sections[0].items) == 2
shown = module._report_display_text(sections[0].items[0])
assert "-1、-0.25、-3.5e-06" in shown
assert shown.count("-3.5e-06") == repeat
assert "NEGATIVE_ARRAY_TAIL" in shown
assert sections[0].items[1] == "独立下一项"
def test_sidebar_tools_do_not_duplicate_on_report_resize_and_scroll(application, monkeypatch):
dialog = module.AiConsultDialog(object(), PermissionSet(["tcm.diagnosis/aiAssistant"]))
monkeypatch.setattr(dialog, "_load_workspace", lambda _: None)
try:
dialog.open_for(diagnosis_id=501, patient_id=1501)
original = "## 一、核对资料\n- " + "完整记录,不编造缺项。" * 100 + "\n## 二、核对结尾\n- 独有结尾"
bubble = dialog._append_bubble("ai", original)
assert bubble.finalize_clinical_analysis()
dialog.show()
for width, height in ((1280, 820), (1635, 3708), (1080, 680), (1440, 900), (1280, 820)):
dialog.resize(width, height)
QTest.qWait(30)
tools = dialog.findChildren(module._ClickCard, "AiConsultToolCard")
suggestions = dialog.findChildren(module._ClickCard, "AiConsultSuggestRow")
assert len(tools) == 6 and len(suggestions) == 5
for card, (_, text, _) in zip(tools, module.TOOL_ITEMS):
labels = card.findChildren(QLabel)
assert sum(label.text() == text for label in labels) == 1
assert len([label for label in labels if not label.pixmap().isNull()]) == 1
for value in (0, dialog.chat_scroll.verticalScrollBar().maximum()):
dialog.chat_scroll.verticalScrollBar().setValue(value)
application.processEvents()
assert bubble._raw_payload == original
assert dialog.findChild(module.QWidget, "AiConsultSideCanvas") is not None
assert dialog.findChild(module.QWidget, "AiConsultSideViewport") is not None
assert bubble.findChildren(QTextBrowser, "AiConsultFittedViewport")
finally:
for timer in dialog.findChildren(QTimer):
timer.stop()
dialog.close()
dialog.deleteLater()
application.sendPostedEvents(None, QEvent.Type.DeferredDelete)
application.processEvents()