| """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" |
| doc = _open(data) |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| 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)) |
| |
| 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" |
| doc = _open(data) |
| texts = "\n".join(p.text for p in doc.paragraphs) |
| assert "前" in texts and "后" in texts |
|
|