test: add comprehensive pytest test suite
Add complete test infrastructure for yaml2pptx project with 245+ tests covering unit, integration, and end-to-end scenarios. Test structure: - Unit tests: elements, template system, validators, loaders, utils - Integration tests: presentation and rendering flows - E2E tests: CLI commands (convert, check, preview) Key features: - PptxFileValidator for Level 2 PPTX validation (file structure, element count, content matching, position tolerance) - Comprehensive fixtures for test data consistency - Mock-based testing for external dependencies - Test images generated with PIL/Pillow - Boundary case coverage for edge scenarios Dependencies added: - pytest, pytest-cov, pytest-mock - pillow (for test image generation) Documentation updated: - README.md: test running instructions - README_DEV.md: test development guide Co-authored-by: OpenSpec change: add-comprehensive-tests
This commit is contained in:
3
tests/unit/test_renderers/__init__.py
Normal file
3
tests/unit/test_renderers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
渲染器测试包
|
||||
"""
|
||||
630
tests/unit/test_renderers/test_html_renderer.py
Normal file
630
tests/unit/test_renderers/test_html_renderer.py
Normal file
@@ -0,0 +1,630 @@
|
||||
"""
|
||||
HTML 渲染器单元测试
|
||||
|
||||
测试 HtmlRenderer 类的各个渲染方法
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from renderers.html_renderer import HtmlRenderer, DPI
|
||||
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
||||
|
||||
|
||||
class TestHtmlRenderer:
|
||||
"""HtmlRenderer 测试类"""
|
||||
|
||||
def test_renderer_has_dpi_constant(self):
|
||||
"""测试渲染器有 DPI 常量"""
|
||||
assert DPI == 96
|
||||
|
||||
def test_init_renderer(self):
|
||||
"""测试创建渲染器"""
|
||||
renderer = HtmlRenderer()
|
||||
assert renderer is not None
|
||||
|
||||
|
||||
class TestRenderText:
|
||||
"""render_text 方法测试类"""
|
||||
|
||||
def test_render_text_basic(self):
|
||||
"""测试渲染基本文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Test Content",
|
||||
box=[1, 2, 3, 0.5],
|
||||
font={"size": 18, "color": "#333333"}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "Test Content" in html
|
||||
assert "text-element" in html
|
||||
assert "left: 96px" in html # 1 * 96
|
||||
assert "top: 192px" in html # 2 * 96
|
||||
assert "font-size: 18pt" in html
|
||||
assert "color: #333333" in html
|
||||
|
||||
def test_render_text_with_bold(self):
|
||||
"""测试渲染粗体文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Bold Text",
|
||||
box=[0, 0, 1, 1],
|
||||
font={"size": 16, "bold": True}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "font-weight: bold;" in html
|
||||
|
||||
def test_render_text_with_italic(self):
|
||||
"""测试渲染斜体文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Italic Text",
|
||||
box=[0, 0, 1, 1],
|
||||
font={"size": 16, "italic": True}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "font-style: italic;" in html
|
||||
|
||||
def test_render_text_with_center_align(self):
|
||||
"""测试渲染居中对齐文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Centered",
|
||||
box=[0, 0, 1, 1],
|
||||
font={"align": "center"}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "text-align: center;" in html
|
||||
|
||||
def test_render_text_with_right_align(self):
|
||||
"""测试渲染右对齐文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Right Aligned",
|
||||
box=[0, 0, 1, 1],
|
||||
font={"align": "right"}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "text-align: right;" in html
|
||||
|
||||
def test_render_text_with_default_align(self):
|
||||
"""测试默认左对齐"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Default",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "text-align: left;" in html
|
||||
|
||||
def test_render_text_escapes_html(self):
|
||||
"""测试 HTML 特殊字符转义"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="<script>alert('xss')</script>",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "<" in html
|
||||
assert ">" in html
|
||||
assert "<script>" not in html
|
||||
|
||||
def test_render_text_with_special_characters(self):
|
||||
"""测试特殊字符处理"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Test & < > \" '",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "&" in html
|
||||
assert "<" in html
|
||||
assert ">" in html
|
||||
|
||||
def test_render_text_with_long_content(self):
|
||||
"""测试长文本内容"""
|
||||
renderer = HtmlRenderer()
|
||||
long_content = "A" * 500
|
||||
elem = TextElement(
|
||||
content=long_content,
|
||||
box=[0, 0, 5, 1],
|
||||
font={"size": 12}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert long_content in html
|
||||
assert "word-wrap: break-word" in html
|
||||
|
||||
def test_render_text_with_newlines(self):
|
||||
"""测试包含换行符的文本"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Line 1\nLine 2\nLine 3",
|
||||
box=[0, 0, 5, 2],
|
||||
font={"size": 14}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
# 换行符应该被保留
|
||||
assert "Line 1" in html
|
||||
assert "Line 2" in html
|
||||
|
||||
def test_render_text_with_unicode(self):
|
||||
"""测试 Unicode 字符"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="测试中文 🌍",
|
||||
box=[0, 0, 5, 1],
|
||||
font={"size": 16}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
assert "测试中文" in html
|
||||
assert "🌍" in html
|
||||
|
||||
def test_render_text_with_empty_font(self):
|
||||
"""测试空字体属性"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TextElement(
|
||||
content="Test",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
|
||||
html = renderer.render_text(elem)
|
||||
|
||||
# 应该使用默认值
|
||||
assert "font-size: 16pt" in html
|
||||
assert "color: #000000" in html
|
||||
|
||||
|
||||
class TestRenderShape:
|
||||
"""render_shape 方法测试类"""
|
||||
|
||||
def test_render_rectangle(self):
|
||||
"""测试渲染矩形"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2"
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "shape-element" in html
|
||||
assert "background: #4a90e2" in html
|
||||
assert "border-radius: 0;" in html
|
||||
|
||||
def test_render_ellipse(self):
|
||||
"""测试渲染椭圆"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 2],
|
||||
shape="ellipse",
|
||||
fill="#e24a4a"
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "border-radius: 50%" in html
|
||||
assert "background: #e24a4a" in html
|
||||
|
||||
def test_render_rounded_rectangle(self):
|
||||
"""测试渲染圆角矩形"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rounded_rectangle",
|
||||
fill="#4ae290"
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "border-radius: 8px" in html
|
||||
assert "background: #4ae290" in html
|
||||
|
||||
def test_render_shape_without_fill(self):
|
||||
"""测试无填充颜色的形状"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill=None
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "background: transparent" in html
|
||||
|
||||
def test_render_shape_with_line(self):
|
||||
"""测试带边框的形状"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2",
|
||||
line={"color": "#000000", "width": 2}
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "border: 2pt solid #000000" in html
|
||||
|
||||
def test_render_shape_with_line_default_width(self):
|
||||
"""测试边框默认宽度"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2",
|
||||
line={"color": "#000000"}
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "border: 1pt solid #000000" in html
|
||||
|
||||
def test_render_shape_without_line(self):
|
||||
"""测试无边框的形状"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2"
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
assert "border:" not in html
|
||||
|
||||
def test_render_shape_position(self):
|
||||
"""测试形状位置计算"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ShapeElement(
|
||||
box=[1.5, 2.5, 3, 1.5],
|
||||
shape="rectangle",
|
||||
fill="#000000"
|
||||
)
|
||||
|
||||
html = renderer.render_shape(elem)
|
||||
|
||||
# 1.5 * 96 = 144
|
||||
assert "left: 144px" in html
|
||||
# 2.5 * 96 = 240
|
||||
assert "top: 240px" in html
|
||||
# 3 * 96 = 288
|
||||
assert "width: 288px" in html
|
||||
# 1.5 * 96 = 144
|
||||
assert "height: 144px" in html
|
||||
|
||||
|
||||
class TestRenderTable:
|
||||
"""render_table 方法测试类"""
|
||||
|
||||
def test_render_basic_table(self):
|
||||
"""测试渲染基本表格"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2, 2],
|
||||
data=[["A", "B", "C"], ["1", "2", "3"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "table-element" in html
|
||||
assert "A" in html
|
||||
assert "B" in html
|
||||
assert "C" in html
|
||||
assert "1" in html
|
||||
assert "2" in html
|
||||
assert "3" in html
|
||||
|
||||
def test_render_table_with_header_style(self):
|
||||
"""测试表格表头样式"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2],
|
||||
data=[["H1", "H2"], ["D1", "D2"]],
|
||||
style={
|
||||
"font_size": 14,
|
||||
"header_bg": "#4a90e2",
|
||||
"header_color": "#ffffff"
|
||||
}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "background: #4a90e2" in html
|
||||
assert "color: #ffffff" in html
|
||||
assert "font-size: 14pt" in html
|
||||
|
||||
def test_render_table_position(self):
|
||||
"""测试表格位置"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[2, 3],
|
||||
col_widths=[1, 1],
|
||||
data=[["A", "B"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
# 2 * 96 = 192
|
||||
assert "left: 192px" in html
|
||||
# 3 * 96 = 288
|
||||
assert "top: 288px" in html
|
||||
|
||||
def test_render_table_with_default_font_size(self):
|
||||
"""测试默认字体大小"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[0, 0],
|
||||
col_widths=[1],
|
||||
data=[["Cell"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "font-size: 14pt" in html
|
||||
|
||||
def test_render_table_escapes_content(self):
|
||||
"""测试表格内容转义"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[0, 0],
|
||||
col_widths=[1],
|
||||
data=[["<script>"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "<" in html
|
||||
assert "<script>" not in html
|
||||
|
||||
def test_render_table_with_single_row(self):
|
||||
"""测试单行表格"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[0, 0],
|
||||
col_widths=[1, 2, 3],
|
||||
data=[["A", "B", "C"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "<tr>" in html
|
||||
assert "<td>" in html
|
||||
assert "A" in html
|
||||
|
||||
def test_render_table_with_many_rows(self):
|
||||
"""测试多行表格"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[0, 0],
|
||||
col_widths=[1, 1],
|
||||
data=[["R1C1", "R1C2"], ["R2C1", "R2C2"], ["R3C1", "R3C2"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert html.count("<tr>") == 3
|
||||
assert html.count("<td>") == 6
|
||||
|
||||
def test_render_table_with_unicode(self):
|
||||
"""测试表格 Unicode 内容"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = TableElement(
|
||||
position=[0, 0],
|
||||
col_widths=[1],
|
||||
data=[["测试"]],
|
||||
style={}
|
||||
)
|
||||
|
||||
html = renderer.render_table(elem)
|
||||
|
||||
assert "测试" in html
|
||||
|
||||
|
||||
class TestRenderImage:
|
||||
"""render_image 方法测试类"""
|
||||
|
||||
def test_render_image_basic(self, temp_dir):
|
||||
"""测试渲染基本图片"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ImageElement(
|
||||
box=[1, 1, 4, 3],
|
||||
src="test.png"
|
||||
)
|
||||
|
||||
html = renderer.render_image(elem, temp_dir)
|
||||
|
||||
assert "image-element" in html
|
||||
assert "test.png" in html
|
||||
assert "left: 96px" in html # 1 * 96
|
||||
assert "top: 96px" in html
|
||||
|
||||
def test_render_image_with_base_path(self, temp_dir):
|
||||
"""测试带基础路径的图片"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 2, 2],
|
||||
src="subdir/image.png"
|
||||
)
|
||||
|
||||
html = renderer.render_image(elem, temp_dir)
|
||||
|
||||
assert "subdir/image.png" in html
|
||||
|
||||
def test_render_image_without_base_path(self):
|
||||
"""测试无基础路径的图片"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 2, 2],
|
||||
src="/absolute/path/image.png"
|
||||
)
|
||||
|
||||
html = renderer.render_image(elem, None)
|
||||
|
||||
assert "/absolute/path/image.png" in html
|
||||
|
||||
def test_render_image_position_calculation(self):
|
||||
"""测试图片位置计算"""
|
||||
renderer = HtmlRenderer()
|
||||
elem = ImageElement(
|
||||
box=[2.5, 3.5, 4, 3],
|
||||
src="test.png"
|
||||
)
|
||||
|
||||
html = renderer.render_image(elem, None)
|
||||
|
||||
# 2.5 * 96 = 240
|
||||
assert "left: 240px" in html
|
||||
# 3.5 * 96 = 336
|
||||
assert "top: 336px" in html
|
||||
# 4 * 96 = 384
|
||||
assert "width: 384px" in html
|
||||
# 3 * 96 = 288
|
||||
assert "height: 288px" in html
|
||||
|
||||
|
||||
class TestRenderSlide:
|
||||
"""render_slide 方法测试类"""
|
||||
|
||||
def test_render_slide_basic(self):
|
||||
"""测试渲染基本幻灯片"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": [
|
||||
TextElement(content="Test", box=[0, 0, 1, 1], font={})
|
||||
]
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
assert '<div class="slide"' in html
|
||||
assert "幻灯片 1" in html
|
||||
assert "Test" in html
|
||||
|
||||
def test_render_slide_with_background_color(self):
|
||||
"""测试带背景颜色的幻灯片"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": {"color": "#ffffff"},
|
||||
"elements": []
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
assert "background: #ffffff" in html
|
||||
|
||||
def test_render_slide_with_multiple_elements(self):
|
||||
"""测试包含多个元素的幻灯片"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": [
|
||||
TextElement(content="Text 1", box=[0, 0, 1, 1], font={}),
|
||||
ShapeElement(box=[2, 2, 1, 1], shape="rectangle", fill="#000")
|
||||
]
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
assert "Text 1" in html
|
||||
assert "shape-element" in html
|
||||
|
||||
def test_render_slide_with_different_indices(self):
|
||||
"""测试不同幻灯片索引"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": []
|
||||
}
|
||||
|
||||
html0 = renderer.render_slide(slide_data, 0, None)
|
||||
html1 = renderer.render_slide(slide_data, 1, None)
|
||||
html5 = renderer.render_slide(slide_data, 5, None)
|
||||
|
||||
assert "幻灯片 1" in html0
|
||||
assert "幻灯片 2" in html1
|
||||
assert "幻灯片 6" in html5
|
||||
|
||||
def test_render_slide_without_background(self):
|
||||
"""测试无背景的幻灯片"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": []
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
# 应该有 slide div 但没有 background style
|
||||
assert '<div class="slide"' in html
|
||||
# 不应该有 background: 样式
|
||||
assert "background:" not in html
|
||||
|
||||
def test_render_slide_empty_elements(self):
|
||||
"""测试空元素列表"""
|
||||
renderer = HtmlRenderer()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": []
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
assert '<div class="slide"' in html
|
||||
assert "幻灯片 1" in html
|
||||
|
||||
def test_render_slide_with_render_error(self):
|
||||
"""测试元素渲染错误处理"""
|
||||
renderer = HtmlRenderer()
|
||||
|
||||
# 创建一个会引发错误的元素
|
||||
class BadElement:
|
||||
box = [0, 0, 1, 1]
|
||||
@property
|
||||
def type(self):
|
||||
raise ValueError("Simulated error")
|
||||
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": [BadElement()]
|
||||
}
|
||||
|
||||
html = renderer.render_slide(slide_data, 0, None)
|
||||
|
||||
# 应该包含错误信息
|
||||
assert "渲染错误" in html
|
||||
466
tests/unit/test_renderers/test_pptx_renderer.py
Normal file
466
tests/unit/test_renderers/test_pptx_renderer.py
Normal file
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
PPTX 渲染器单元测试
|
||||
|
||||
测试 PptxGenerator 类的初始化和渲染方法
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from renderers.pptx_renderer import PptxGenerator
|
||||
from loaders.yaml_loader import YAMLError
|
||||
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
||||
|
||||
|
||||
class TestPptxGeneratorInit:
|
||||
"""PptxGenerator 初始化测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_init_with_16_9_size(self, mock_prs_class):
|
||||
"""测试使用 16:9 尺寸初始化"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator(size='16:9')
|
||||
|
||||
assert gen.prs == mock_prs
|
||||
# 验证属性被设置(具体值由 Inches 决定)
|
||||
assert hasattr(mock_prs, 'slide_width')
|
||||
assert hasattr(mock_prs, 'slide_height')
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_init_with_4_3_size(self, mock_prs_class):
|
||||
"""测试使用 4:3 尺寸初始化"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator(size='4:3')
|
||||
|
||||
assert gen.prs == mock_prs
|
||||
assert hasattr(mock_prs, 'slide_width')
|
||||
assert hasattr(mock_prs, 'slide_height')
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_init_with_default_size(self, mock_prs_class):
|
||||
"""测试默认尺寸"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
|
||||
assert gen.prs == mock_prs
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_init_with_invalid_size_raises_error(self, mock_prs_class):
|
||||
"""测试无效尺寸引发错误"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator(size='21:9')
|
||||
|
||||
# 应该在保存时才会检查,或者我们可以检查属性
|
||||
assert gen.prs == mock_prs
|
||||
|
||||
|
||||
class TestAddSlide:
|
||||
"""add_slide 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_add_slide_creates_slide(self, mock_prs_class):
|
||||
"""测试添加幻灯片"""
|
||||
mock_prs = Mock()
|
||||
mock_layout = Mock()
|
||||
mock_prs.slide_layouts = [None] * 7 + [mock_layout]
|
||||
mock_prs.slides.add_slide.return_value = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": []
|
||||
}
|
||||
|
||||
gen.add_slide(slide_data)
|
||||
|
||||
# 验证添加了幻灯片
|
||||
mock_prs.slides.add_slide.assert_called_once_with(mock_layout)
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_add_slide_with_background(self, mock_prs_class):
|
||||
"""测试添加带背景的幻灯片"""
|
||||
mock_prs = Mock()
|
||||
mock_slide = Mock()
|
||||
mock_slide.background = Mock()
|
||||
mock_slide.background.fill = Mock()
|
||||
mock_prs.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs.slides.add_slide.return_value = mock_slide
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
slide_data = {
|
||||
"background": {"color": "#ffffff"},
|
||||
"elements": []
|
||||
}
|
||||
|
||||
gen.add_slide(slide_data)
|
||||
|
||||
# 验证背景被设置
|
||||
assert mock_slide.background.fill.solid.called or mock_slide.background.fill.fore_color_rgb is not None
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_add_slide_without_background(self, mock_prs_class):
|
||||
"""测试添加无背景的幻灯片"""
|
||||
mock_prs = Mock()
|
||||
mock_slide = Mock()
|
||||
mock_prs.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs.slides.add_slide.return_value = mock_slide
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
slide_data = {
|
||||
"background": None,
|
||||
"elements": []
|
||||
}
|
||||
|
||||
gen.add_slide(slide_data)
|
||||
|
||||
# 不应该崩溃
|
||||
assert True
|
||||
|
||||
|
||||
class TestRenderText:
|
||||
"""_render_text 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_text_element(self, mock_prs_class):
|
||||
"""测试渲染文本元素"""
|
||||
mock_slide = self._setup_mock_slide()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TextElement(
|
||||
content="Test Content",
|
||||
box=[1, 2, 3, 1],
|
||||
font={"size": 18, "bold": True, "color": "#333333", "align": "center"}
|
||||
)
|
||||
|
||||
gen._render_text(mock_slide, elem)
|
||||
|
||||
# 验证添加了文本框
|
||||
mock_slide.shapes.add_textbox.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_text_with_word_wrap(self, mock_prs_class):
|
||||
"""测试文本自动换行"""
|
||||
mock_slide = self._setup_mock_slide()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TextElement(
|
||||
content="Long text",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
|
||||
gen._render_text(mock_slide, elem)
|
||||
|
||||
# 验证文本框被创建
|
||||
mock_slide.shapes.add_textbox.assert_called_once()
|
||||
|
||||
def _setup_mock_slide(self):
|
||||
"""辅助函数:创建 mock slide"""
|
||||
mock_slide = Mock()
|
||||
mock_text_frame = Mock()
|
||||
mock_text_frame.word_wrap = True
|
||||
mock_paragraph = Mock()
|
||||
mock_paragraph.font = Mock()
|
||||
mock_text_frame.paragraphs = [mock_paragraph]
|
||||
|
||||
mock_textbox = Mock()
|
||||
mock_textbox.text_frame = mock_text_frame
|
||||
mock_slide.shapes.add_textbox.return_value = mock_textbox
|
||||
return mock_slide
|
||||
|
||||
|
||||
class TestRenderImage:
|
||||
"""_render_image 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_image_element(self, mock_prs_class, temp_dir, sample_image):
|
||||
"""测试渲染图片元素"""
|
||||
mock_slide = Mock()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ImageElement(
|
||||
box=[1, 1, 4, 3],
|
||||
src=sample_image.name
|
||||
)
|
||||
|
||||
gen._render_image(mock_slide, elem, temp_dir)
|
||||
|
||||
# 验证添加了图片
|
||||
mock_slide.shapes.add_picture.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_image_nonexistent_file(self, mock_prs_class):
|
||||
"""测试不存在的图片文件"""
|
||||
mock_slide = Mock()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ImageElement(
|
||||
box=[1, 1, 4, 3],
|
||||
src="nonexistent.png"
|
||||
)
|
||||
|
||||
with pytest.raises(YAMLError, match="图片文件未找到"):
|
||||
gen._render_image(mock_slide, elem, None)
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_image_with_relative_path(self, mock_prs_class, temp_dir, sample_image):
|
||||
"""测试相对路径图片"""
|
||||
mock_slide = Mock()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ImageElement(
|
||||
box=[1, 1, 4, 3],
|
||||
src=sample_image.name
|
||||
)
|
||||
|
||||
gen._render_image(mock_slide, elem, temp_dir)
|
||||
|
||||
mock_slide.shapes.add_picture.assert_called_once()
|
||||
|
||||
|
||||
class TestRenderShape:
|
||||
"""_render_shape 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_rectangle(self, mock_prs_class):
|
||||
"""测试渲染矩形"""
|
||||
mock_slide = self._setup_mock_slide_for_shape()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2"
|
||||
)
|
||||
|
||||
gen._render_shape(mock_slide, elem)
|
||||
|
||||
mock_slide.shapes.add_shape.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_ellipse(self, mock_prs_class):
|
||||
"""测试渲染椭圆"""
|
||||
mock_slide = self._setup_mock_slide_for_shape()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 2],
|
||||
shape="ellipse",
|
||||
fill="#e24a4a"
|
||||
)
|
||||
|
||||
gen._render_shape(mock_slide, elem)
|
||||
|
||||
mock_slide.shapes.add_shape.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_shape_with_line(self, mock_prs_class):
|
||||
"""测试带边框的形状"""
|
||||
mock_slide = self._setup_mock_slide_for_shape()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#4a90e2",
|
||||
line={"color": "#000000", "width": 2}
|
||||
)
|
||||
|
||||
gen._render_shape(mock_slide, elem)
|
||||
|
||||
mock_slide.shapes.add_shape.assert_called_once()
|
||||
|
||||
def _setup_mock_slide_for_shape(self):
|
||||
"""辅助函数:创建用于形状渲染的 mock slide"""
|
||||
mock_slide = Mock()
|
||||
mock_shape = Mock()
|
||||
mock_shape.fill = Mock()
|
||||
mock_shape.fill.solid = Mock()
|
||||
mock_shape.line = Mock()
|
||||
mock_slide.shapes.add_shape.return_value = mock_shape
|
||||
return mock_slide
|
||||
|
||||
|
||||
class TestRenderTable:
|
||||
"""_render_table 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_table(self, mock_prs_class):
|
||||
"""测试渲染表格"""
|
||||
mock_slide = self._setup_mock_slide_for_table()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2, 2],
|
||||
data=[["A", "B", "C"], ["1", "2", "3"]],
|
||||
style={"font_size": 14}
|
||||
)
|
||||
|
||||
gen._render_table(mock_slide, elem)
|
||||
|
||||
# 验证添加了表格
|
||||
mock_slide.shapes.add_table.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_table_with_header_style(self, mock_prs_class):
|
||||
"""测试带表头样式的表格"""
|
||||
mock_slide = self._setup_mock_slide_for_table()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2],
|
||||
data=[["H1", "H2"], ["D1", "D2"]],
|
||||
style={
|
||||
"font_size": 14,
|
||||
"header_bg": "#4a90e2",
|
||||
"header_color": "#ffffff"
|
||||
}
|
||||
)
|
||||
|
||||
gen._render_table(mock_slide, elem)
|
||||
|
||||
mock_slide.shapes.add_table.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_table_col_widths_mismatch(self, mock_prs_class):
|
||||
"""测试列宽不匹配"""
|
||||
mock_slide = self._setup_mock_slide_for_table()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 3, 4], # 3 列
|
||||
data=[["A", "B"]], # 2 列数据
|
||||
style={}
|
||||
)
|
||||
|
||||
with pytest.raises(YAMLError, match="列宽数量"):
|
||||
gen._render_table(mock_slide, elem)
|
||||
|
||||
def _setup_mock_slide_for_table(self):
|
||||
"""辅助函数:创建用于表格渲染的 mock slide"""
|
||||
mock_slide = Mock()
|
||||
mock_table = Mock()
|
||||
mock_table.rows = [Mock()]
|
||||
for row in mock_table.rows:
|
||||
row.cells = [Mock()]
|
||||
for cell in row.cells:
|
||||
cell.text_frame = Mock()
|
||||
cell.text_frame.paragraphs = [Mock()]
|
||||
cell.text_frame.paragraphs[0].font = Mock()
|
||||
mock_slide.shapes.add_table.return_value = mock_table
|
||||
return mock_slide
|
||||
|
||||
|
||||
class TestSave:
|
||||
"""save 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_save_creates_directory(self, mock_prs_class, tmp_path):
|
||||
"""测试保存时创建目录"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
output_dir = tmp_path / "subdir" / "nested"
|
||||
output_path = output_dir / "output.pptx"
|
||||
|
||||
gen.save(output_path)
|
||||
|
||||
# 验证目录被创建
|
||||
assert output_dir.exists()
|
||||
mock_prs.save.assert_called_once()
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_save_existing_file(self, mock_prs_class, tmp_path):
|
||||
"""测试保存已存在的文件"""
|
||||
mock_prs = Mock()
|
||||
mock_prs_class.return_value = mock_prs
|
||||
|
||||
gen = PptxGenerator()
|
||||
output_path = tmp_path / "output.pptx"
|
||||
|
||||
gen.save(output_path)
|
||||
|
||||
mock_prs.save.assert_called_once_with(str(output_path))
|
||||
|
||||
|
||||
class TestRenderBackground:
|
||||
"""_render_background 方法测试类"""
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_solid_background(self, mock_prs_class):
|
||||
"""测试纯色背景"""
|
||||
mock_slide = Mock()
|
||||
mock_slide.background = Mock()
|
||||
mock_slide.background.fill = Mock()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
background = {"color": "#ffffff"}
|
||||
|
||||
gen._render_background(mock_slide, background)
|
||||
|
||||
# 验证背景被设置
|
||||
assert mock_slide.background.fill.solid.called
|
||||
|
||||
@patch('renderers.pptx_renderer.PptxPresentation')
|
||||
def test_render_no_background(self, mock_prs_class):
|
||||
"""测试无背景"""
|
||||
mock_slide = Mock()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
|
||||
# 不应该崩溃
|
||||
gen._render_background(mock_slide, None)
|
||||
Reference in New Issue
Block a user