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