File size: 5,635 Bytes
0e6887b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """HTML → 专业 Word(.docx) 转换器测试。
覆盖:表格转 Word 表格、标题分级、Markdown/AI 噪声剥离、style/svg 丢弃、
中文与产物可被 python-docx 重新打开(结构有效)。
"""
from __future__ import annotations
from io import BytesIO
import pytest
from services.docx_export import html_to_docx, docx_available
pytestmark = pytest.mark.skipif(not docx_available(), reason="python-docx 未安装")
_SAMPLE_HTML = """
<!DOCTYPE html><html><head><style>.x{color:red}</style><title>T</title></head>
<body><div class="report-container">
<div class="report-header">
<div class="brand-name">Pharma K</div>
<div class="brand-subtitle">专家系统</div>
<div class="report-type">质量属性梳理报告</div>
<div class="report-date">2026-06-07 00:56</div>
</div>
<div class="kpi-row">
<div class="kpi-card"><div class="kpi-value">37</div><div class="kpi-label">分组</div></div>
<div class="kpi-card"><div class="kpi-value">64</div><div class="kpi-label">观测</div></div>
</div>
<div class="section">
<div class="section-title">各质量属性梳理</div>
<div class="section-body">
<p>溶化时限符合限度。**这段含粗体**和 *斜体* 应被清理。</p>
<p>* 这是一个伪列表行</p>
</div>
</div>
<div class="section">
<div class="section-title">含量均匀度(接受值判定)</div>
<div class="section-body">
<table><thead><tr><th>规格</th><th>AV</th><th>判定</th></tr></thead>
<tbody><tr><td>20μg</td><td>5.074</td><td>符合</td></tr>
<tr><td>40μg</td><td>3.418</td><td>符合</td></tr></tbody></table>
</div>
</div>
<svg><rect/></svg>
</div></body></html>
"""
def _open(data: bytes):
from docx import Document
return Document(BytesIO(data))
def test_returns_valid_docx_bytes():
data = html_to_docx(_SAMPLE_HTML)
assert data and data[:2] == b"PK" # docx 是 zip,魔数 PK
doc = _open(data) # 可被 python-docx 重新打开
assert doc is not None
def test_tables_become_real_word_tables():
doc = _open(html_to_docx(_SAMPLE_HTML))
assert len(doc.tables) >= 1
cu = [t for t in doc.tables if t.rows and t.rows[0].cells[0].text == "规格"]
assert cu, "含量均匀度表应转为 Word 表格且表头为规格"
tbl = cu[0]
assert tbl.rows[0].cells[1].text == "AV"
assert tbl.cell(1, 0).text == "20μg"
assert tbl.cell(1, 2).text == "符合"
def test_markdown_and_ai_noise_stripped():
doc = _open(html_to_docx(_SAMPLE_HTML))
all_text = "\n".join(p.text for p in doc.paragraphs)
assert "**" not in all_text and "*" not in all_text
assert "这段含粗体" in all_text
assert "斜体" in all_text
def test_style_and_svg_dropped():
doc = _open(html_to_docx(_SAMPLE_HTML))
all_text = "\n".join(p.text for p in doc.paragraphs)
assert "color:red" not in all_text
assert "rect" not in all_text
def test_headings_present():
doc = _open(html_to_docx(_SAMPLE_HTML))
texts = [p.text for p in doc.paragraphs]
assert "Pharma K" in texts
assert any("各质量属性梳理" in t for t in texts)
def test_empty_html_returns_none():
assert html_to_docx("") is None
def test_meta_void_tags_do_not_swallow_body():
"""回归:<head> 中的 void 元素 <meta>(无闭合)不得吞掉 <body>(致空文档)。"""
html = (
'<!DOCTYPE html><html lang="zh-CN"><head>'
'<meta charset="UTF-8">'
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
'<title>T</title><style>.x{color:red}</style></head>'
'<body><div class="section"><div class="section-title">分节标题</div>'
'<div class="section-body"><p>正文内容</p></div></div></body></html>'
)
doc = _open(html_to_docx(html))
texts = [p.text for p in doc.paragraphs]
assert any("分节标题" in t for t in texts)
assert any("正文内容" in t for t in texts)
# ---------------------------------------------------------------------------
# adaptive-report-visualization 任务 12:内联 PNG → Word 图片
# ---------------------------------------------------------------------------
def _real_png_datauri():
"""用 ChartService 生成一张真实 PNG 的 data-uri(需 matplotlib)。"""
pytest.importorskip("matplotlib")
from services.chart_service import ChartService
res = ChartService().grouped_bar(["A", "B"], [1.0, 2.0], title="t")
assert res.ok
return res.image_base64
def test_inline_png_embedded_as_word_picture():
png = _real_png_datauri()
html = (
"<html><body><div class='section'>"
"<div class='section-title'>图表化呈现</div>"
f"<figure><img src=\"{png}\"><figcaption>含量均匀度</figcaption></figure>"
"<table><tr><th>单位</th><th>值</th></tr><tr><td>1</td><td>99.19</td></tr></table>"
"</div></body></html>"
)
doc = _open(html_to_docx(html))
# 图片被嵌入为 inline shape;配套数据表保留。
assert len(doc.inline_shapes) >= 1
assert len(doc.tables) >= 1
assert any("99.19" in c.text for t in doc.tables for r in t.rows for c in r.cells)
def test_bad_data_uri_skipped_without_crash():
html = "<html><body><p>前</p><img src='data:image/png;base64,@@@bad@@@'><p>后</p></body></html>"
data = html_to_docx(html)
assert data and data[:2] == b"PK" # 不崩溃,仍产出有效 docx
doc = _open(data)
texts = "\n".join(p.text for p in doc.paragraphs)
assert "前" in texts and "后" in texts
|