bidmaster-cli/tests/unit/test_word.py
sladro c1292fcacc feat: add validation and toc pipeline upgrades
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-11-19 10:11:21 +08:00

197 lines
6.8 KiB
Python

"""Word处理器章节填充逻辑单元测试"""
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from docx import Document
from bidmaster.tools.word import WordProcessor
@pytest.fixture()
def word_processor() -> WordProcessor:
return WordProcessor()
def _create_document(path: Path, headings: list[tuple[str, int]]) -> None:
doc = Document()
for text, level in headings:
doc.add_heading(text, level=level)
doc.save(path)
def _count_heading_paragraphs(doc: Document) -> int:
return sum(1 for para in doc.paragraphs if para.style.name.startswith("Heading"))
def test_fill_chapter_with_numbered_heading(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "numbered.docx"
_create_document(doc_path, [("1 商务和技术偏差表", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "商务和技术偏差表",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
"heading_number": "1",
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "测试段落内容")
doc = Document(doc_path)
assert any("测试段落内容" in para.text for para in doc.paragraphs)
def test_fill_chapter_without_number_uses_title(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "title_only.docx"
_create_document(doc_path, [("商务和技术偏差表", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "商务和技术偏差表",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
"heading_number": None,
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "服务方案内容")
doc = Document(doc_path)
assert any("服务方案内容" in para.text for para in doc.paragraphs)
def test_fill_chapter_sequence_fallback(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "sequence.docx"
_create_document(
doc_path,
[
("项目背景", 1),
("售后服务和质保期服务计划", 1),
],
)
chapter_meta = {
"id": "chapter_11",
"title": "售后服务和质保期服务计划",
"level": 1,
"placeholder": "{{chapter_11_content}}",
"normalized_title": word_processor._normalize_heading_text("售后服务和质保期服务计划"),
"heading_number": None,
"order_index": 2,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "售后保障措施")
doc = Document(doc_path)
assert any("售后保障措施" in para.text for para in doc.paragraphs)
def test_fill_chapter_appends_when_not_found(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "fallback.docx"
_create_document(doc_path, [("项目概述", 1)])
chapter_meta = {
"id": "chapter_missing",
"title": "未匹配章节",
"level": 1,
"placeholder": "{{chapter_missing_content}}",
"normalized_title": word_processor._normalize_heading_text("未匹配章节"),
"heading_number": None,
"order_index": 2,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "新增内容")
doc = Document(doc_path)
assert any("【系统提示】" in para.text for para in doc.paragraphs)
assert any("新增内容" in para.text for para in doc.paragraphs)
def test_fill_does_not_create_additional_headings(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "no_new_headings.docx"
_create_document(
doc_path,
[
("1 总体方案", 1),
("1.1 服务范围", 2),
],
)
before = Document(doc_path)
before_count = _count_heading_paragraphs(before)
chapter_meta = {
"id": "chapter_1_1",
"title": "服务范围",
"level": 2,
"placeholder": "{{chapter_1_1_content}}",
"normalized_title": word_processor._normalize_heading_text("服务范围"),
"heading_number": "1.1",
"order_index": 1,
}
content = "## 小节标题\n服务内容描述"
word_processor.fill_chapter_content(doc_path, chapter_meta, content)
after = Document(doc_path)
after_count = _count_heading_paragraphs(after)
assert after_count == before_count
def test_markdown_headings_render_as_bold_paragraphs(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "markdown.docx"
_create_document(doc_path, [("1 技术方案", 1)])
chapter_meta = {
"id": "chapter_1",
"title": "技术方案",
"level": 1,
"placeholder": "{{chapter_1_content}}",
"normalized_title": word_processor._normalize_heading_text("技术方案"),
"heading_number": "1",
"order_index": 1,
}
word_processor.fill_chapter_content(
doc_path,
chapter_meta,
"## 子标题\n段落内容",
)
doc = Document(doc_path)
bold_paras = [para for para in doc.paragraphs if para.text.strip() == "子标题"]
assert bold_paras, "应生成加粗段落"
assert all(not para.style.name.startswith("Heading") for para in bold_paras)
assert any(any(run.bold for run in para.runs) for para in bold_paras)
def test_fill_matches_higher_level_heading(word_processor: WordProcessor) -> None:
with TemporaryDirectory() as tmp_dir:
doc_path = Path(tmp_dir) / "deep_heading.docx"
_create_document(doc_path, [("1.1.1.1 深度章节", 4)])
chapter_meta = {
"id": "chapter_1_1_1_1",
"title": "深度章节",
"level": 4,
"placeholder": "{{chapter_1_1_1_1_content}}",
"normalized_title": word_processor._normalize_heading_text("深度章节"),
"heading_number": "1.1.1.1",
"order_index": 1,
}
word_processor.fill_chapter_content(doc_path, chapter_meta, "深度内容")
doc = Document(doc_path)
assert any("深度内容" in para.text for para in doc.paragraphs)