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:
466
tests/unit/test_elements.py
Normal file
466
tests/unit/test_elements.py
Normal file
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
元素类单元测试
|
||||
|
||||
测试 TextElement、ImageElement、ShapeElement、TableElement 和 create_element 工厂函数
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from core.elements import (
|
||||
TextElement, ImageElement, ShapeElement, TableElement,
|
||||
create_element, _is_valid_color
|
||||
)
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
|
||||
# ============= TextElement 测试 =============
|
||||
|
||||
class TestTextElement:
|
||||
"""TextElement 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 TextElement"""
|
||||
elem = TextElement()
|
||||
assert elem.type == 'text'
|
||||
assert elem.content == ''
|
||||
assert elem.box == [1, 1, 8, 1]
|
||||
assert elem.font == {}
|
||||
|
||||
def test_create_with_values(self):
|
||||
"""测试使用指定值创建 TextElement"""
|
||||
elem = TextElement(
|
||||
content="Test",
|
||||
box=[0, 0, 5, 1],
|
||||
font={"size": 24, "bold": True}
|
||||
)
|
||||
assert elem.content == "Test"
|
||||
assert elem.box == [0, 0, 5, 1]
|
||||
assert elem.font["size"] == 24
|
||||
assert elem.font["bold"] is True
|
||||
|
||||
def test_box_must_be_list_of_four(self):
|
||||
"""测试 box 必须是包含 4 个数字的列表"""
|
||||
with pytest.raises(ValueError, match="box 必须是包含 4 个数字的列表"):
|
||||
TextElement(box=[1, 2, 3]) # 只有 3 个元素
|
||||
|
||||
with pytest.raises(ValueError, match="box 必须是包含 4 个数字的列表"):
|
||||
TextElement(box="not a list")
|
||||
|
||||
def test_validate_invalid_color(self):
|
||||
"""测试无效颜色格式验证"""
|
||||
elem = TextElement(
|
||||
font={"color": "red"} # 无效格式
|
||||
)
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "INVALID_COLOR_FORMAT"
|
||||
|
||||
def test_validate_font_too_small(self):
|
||||
"""测试字体太小警告"""
|
||||
elem = TextElement(font={"size": 6})
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "FONT_TOO_SMALL"
|
||||
|
||||
def test_validate_font_too_large(self):
|
||||
"""测试字体太大警告"""
|
||||
elem = TextElement(font={"size": 120})
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "FONT_TOO_LARGE"
|
||||
|
||||
def test_validate_valid_font_size(self):
|
||||
"""测试有效字体大小不产生警告"""
|
||||
elem = TextElement(font={"size": 18})
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
# ============= ImageElement 测试 =============
|
||||
|
||||
class TestImageElement:
|
||||
"""ImageElement 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 ImageElement"""
|
||||
elem = ImageElement(src="test.png")
|
||||
assert elem.type == 'image'
|
||||
assert elem.src == "test.png"
|
||||
assert elem.box == [1, 1, 4, 3]
|
||||
|
||||
def test_empty_src_raises_error(self):
|
||||
"""测试空 src 会引发错误"""
|
||||
with pytest.raises(ValueError, match="图片元素必须指定 src"):
|
||||
ImageElement(src="")
|
||||
|
||||
def test_box_must_be_list_of_four(self):
|
||||
"""测试 box 必须是包含 4 个数字的列表"""
|
||||
with pytest.raises(ValueError, match="box 必须是包含 4 个数字的列表"):
|
||||
ImageElement(src="test.png", box=[1, 2, 3])
|
||||
|
||||
def test_validate_returns_empty_list(self):
|
||||
"""测试 ImageElement.validate() 返回空列表"""
|
||||
elem = ImageElement(src="test.png")
|
||||
issues = elem.validate()
|
||||
assert issues == []
|
||||
|
||||
|
||||
# ============= ShapeElement 测试 =============
|
||||
|
||||
class TestShapeElement:
|
||||
"""ShapeElement 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 ShapeElement"""
|
||||
elem = ShapeElement()
|
||||
assert elem.type == 'shape'
|
||||
assert elem.shape == 'rectangle'
|
||||
assert elem.box == [1, 1, 2, 1]
|
||||
assert elem.fill is None
|
||||
assert elem.line is None
|
||||
|
||||
def test_box_must_be_list_of_four(self):
|
||||
"""测试 box 必须是包含 4 个数字的列表"""
|
||||
with pytest.raises(ValueError, match="box 必须是包含 4 个数字的列表"):
|
||||
ShapeElement(box=[1, 2, 3])
|
||||
|
||||
def test_validate_invalid_shape_type(self):
|
||||
"""测试无效形状类型"""
|
||||
elem = ShapeElement(shape="triangle")
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "INVALID_SHAPE_TYPE"
|
||||
assert "triangle" in issues[0].message
|
||||
|
||||
def test_validate_valid_shape_types(self):
|
||||
"""测试有效的形状类型"""
|
||||
for shape_type in ['rectangle', 'ellipse', 'rounded_rectangle']:
|
||||
elem = ShapeElement(shape=shape_type)
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_invalid_fill_color(self):
|
||||
"""测试无效填充颜色"""
|
||||
elem = ShapeElement(fill="red")
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].code == "INVALID_COLOR_FORMAT"
|
||||
|
||||
def test_validate_invalid_line_color(self):
|
||||
"""测试无效线条颜色"""
|
||||
elem = ShapeElement(line={"color": "blue"})
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].code == "INVALID_COLOR_FORMAT"
|
||||
|
||||
def test_validate_valid_colors(self):
|
||||
"""测试有效的颜色"""
|
||||
elem = ShapeElement(
|
||||
fill="#4a90e2",
|
||||
line={"color": "#000000", "width": 2}
|
||||
)
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
# ============= TableElement 测试 =============
|
||||
|
||||
class TestTableElement:
|
||||
"""TableElement 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 TableElement"""
|
||||
elem = TableElement(data=[["A", "B"]])
|
||||
assert elem.type == 'table'
|
||||
assert elem.data == [["A", "B"]]
|
||||
assert elem.position == [1, 1]
|
||||
assert elem.col_widths == []
|
||||
assert elem.style == {}
|
||||
|
||||
def test_empty_data_raises_error(self):
|
||||
"""测试空数据会引发错误"""
|
||||
with pytest.raises(ValueError, match="表格数据不能为空"):
|
||||
TableElement(data=[])
|
||||
|
||||
def test_position_must_be_list_of_two(self):
|
||||
"""测试 position 必须是包含 2 个数字的列表"""
|
||||
with pytest.raises(ValueError, match="position 必须是包含 2 个数字的列表"):
|
||||
TableElement(data=[["A"]], position=[1])
|
||||
|
||||
def test_validate_inconsistent_columns(self):
|
||||
"""测试行列数不一致"""
|
||||
elem = TableElement(data=[
|
||||
["A", "B", "C"],
|
||||
["X", "Y"], # 只有 2 列
|
||||
["1", "2", "3"]
|
||||
])
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TABLE_INCONSISTENT_COLUMNS"
|
||||
|
||||
def test_validate_col_widths_mismatch(self):
|
||||
"""测试 col_widths 与列数不匹配"""
|
||||
elem = TableElement(
|
||||
data=[["A", "B"], ["C", "D"]],
|
||||
col_widths=[1, 2, 3] # 3 列但数据只有 2 列
|
||||
)
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "TABLE_COL_WIDTHS_MISMATCH"
|
||||
|
||||
def test_validate_consistent_table(self):
|
||||
"""测试一致的表格数据"""
|
||||
elem = TableElement(
|
||||
data=[["A", "B"], ["C", "D"]],
|
||||
col_widths=[2, 2]
|
||||
)
|
||||
issues = elem.validate()
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
# ============= create_element 工厂函数测试 =============
|
||||
|
||||
class TestCreateElement:
|
||||
"""create_element 工厂函数测试类"""
|
||||
|
||||
def test_create_text_element(self):
|
||||
"""测试创建文本元素"""
|
||||
elem = create_element({"type": "text", "content": "Test"})
|
||||
assert isinstance(elem, TextElement)
|
||||
assert elem.content == "Test"
|
||||
|
||||
def test_create_image_element(self):
|
||||
"""测试创建图片元素"""
|
||||
elem = create_element({"type": "image", "src": "test.png"})
|
||||
assert isinstance(elem, ImageElement)
|
||||
assert elem.src == "test.png"
|
||||
|
||||
def test_create_shape_element(self):
|
||||
"""测试创建形状元素"""
|
||||
elem = create_element({"type": "shape", "shape": "ellipse"})
|
||||
assert isinstance(elem, ShapeElement)
|
||||
assert elem.shape == "ellipse"
|
||||
|
||||
def test_create_table_element(self):
|
||||
"""测试创建表格元素"""
|
||||
elem = create_element({"type": "table", "data": [["A", "B"]]})
|
||||
assert isinstance(elem, TableElement)
|
||||
assert elem.data == [["A", "B"]]
|
||||
|
||||
def test_unsupported_type_raises_error(self):
|
||||
"""测试不支持的元素类型"""
|
||||
with pytest.raises(ValueError, match="不支持的元素类型"):
|
||||
create_element({"type": "video"})
|
||||
|
||||
def test_missing_type_raises_error(self):
|
||||
"""测试缺少 type 字段"""
|
||||
with pytest.raises(ValueError, match="不支持的元素类型"):
|
||||
create_element({"content": "Test"})
|
||||
|
||||
|
||||
# ============= _is_valid_color 工具函数测试 =============
|
||||
|
||||
class TestIsValidColor:
|
||||
"""_is_valid_color 工具函数测试类"""
|
||||
|
||||
def test_valid_full_hex_color(self):
|
||||
"""测试完整的 #RRGGBB 格式"""
|
||||
assert _is_valid_color("#ffffff") is True
|
||||
assert _is_valid_color("#000000") is True
|
||||
assert _is_valid_color("#4a90e2") is True
|
||||
assert _is_valid_color("#FF0000") is True
|
||||
|
||||
def test_valid_short_hex_color(self):
|
||||
"""测试短格式 #RGB"""
|
||||
assert _is_valid_color("#fff") is True
|
||||
assert _is_valid_color("#000") is True
|
||||
assert _is_valid_color("#abc") is True
|
||||
|
||||
def test_invalid_colors(self):
|
||||
"""测试无效颜色"""
|
||||
assert _is_valid_color("red") is False
|
||||
assert _is_valid_color("#gggggg") is False
|
||||
assert _is_valid_color("ffffff") is False # 缺少 #
|
||||
assert _is_valid_color("#ffff") is False # 4 位
|
||||
assert _is_valid_color("#fffff") is False # 5 位
|
||||
assert _is_valid_color("") is False
|
||||
|
||||
|
||||
# ============= 边界情况补充测试 =============
|
||||
|
||||
class TestTextElementBoundaryCases:
|
||||
"""TextElement 边界情况测试"""
|
||||
|
||||
def test_text_with_very_long_content(self):
|
||||
"""测试非常长的文本内容"""
|
||||
long_content = "A" * 1000
|
||||
elem = TextElement(
|
||||
content=long_content,
|
||||
box=[0, 0, 5, 1],
|
||||
font={"size": 12}
|
||||
)
|
||||
assert elem.content == long_content
|
||||
|
||||
def test_text_with_newline_characters(self):
|
||||
"""测试包含换行符的文本"""
|
||||
elem = TextElement(
|
||||
content="Line 1\nLine 2\nLine 3",
|
||||
box=[0, 0, 5, 2],
|
||||
font={}
|
||||
)
|
||||
assert "\n" in elem.content
|
||||
|
||||
def test_text_with_tab_characters(self):
|
||||
"""测试包含制表符的文本"""
|
||||
elem = TextElement(
|
||||
content="Col1\tCol2\tCol3",
|
||||
box=[0, 0, 5, 1],
|
||||
font={}
|
||||
)
|
||||
assert "\t" in elem.content
|
||||
|
||||
def test_text_empty_font_size(self):
|
||||
"""测试空字体大小"""
|
||||
elem = TextElement(
|
||||
content="Test",
|
||||
box=[0, 0, 1, 1],
|
||||
font={}
|
||||
)
|
||||
# 默认值应该有效
|
||||
assert "size" not in elem.font
|
||||
|
||||
def test_text_with_color_variations(self):
|
||||
"""测试不同颜色格式"""
|
||||
# 短格式应该被 _is_valid_color 接受,但元素也接受
|
||||
valid_colors = ["#fff", "#FFF", "#000", "#abc", "#ABC"]
|
||||
for color in valid_colors:
|
||||
elem = TextElement(
|
||||
content="Test",
|
||||
box=[0, 0, 1, 1],
|
||||
font={"color": color}
|
||||
)
|
||||
assert elem.font["color"] == color
|
||||
|
||||
|
||||
class TestTableElementBoundaryCases:
|
||||
"""TableElement 边界情况测试"""
|
||||
|
||||
def test_table_with_single_cell(self):
|
||||
"""测试单单元格表格"""
|
||||
elem = TableElement(
|
||||
data=[["Single Cell"]],
|
||||
position=[0, 0]
|
||||
)
|
||||
assert len(elem.data) == 1
|
||||
assert len(elem.data[0]) == 1
|
||||
|
||||
def test_table_with_many_columns(self):
|
||||
"""测试多列表格"""
|
||||
many_cols = ["Col" + str(i) for i in range(20)]
|
||||
elem = TableElement(
|
||||
data=[many_cols],
|
||||
position=[0, 0],
|
||||
col_widths=[1] * 20
|
||||
)
|
||||
assert len(elem.data[0]) == 20
|
||||
|
||||
def test_table_with_single_row(self):
|
||||
"""测试单行表格"""
|
||||
elem = TableElement(
|
||||
data=[["A", "B", "C"]],
|
||||
position=[0, 0]
|
||||
)
|
||||
assert len(elem.data) == 1
|
||||
|
||||
def test_table_with_many_rows(self):
|
||||
"""测试多行表格"""
|
||||
many_rows = [["Row" + str(i)] * 3 for i in range(50)]
|
||||
elem = TableElement(
|
||||
data=many_rows,
|
||||
position=[0, 0],
|
||||
col_widths=[1, 1, 1]
|
||||
)
|
||||
assert len(elem.data) == 50
|
||||
|
||||
|
||||
class TestShapeElementBoundaryCases:
|
||||
"""ShapeElement 边界情况测试"""
|
||||
|
||||
def test_shape_without_line_attribute(self):
|
||||
"""测试无边框的形状"""
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#000000",
|
||||
line=None
|
||||
)
|
||||
assert elem.line is None
|
||||
|
||||
def test_shape_with_empty_line(self):
|
||||
"""测试空 line 字典"""
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 2, 1],
|
||||
shape="rectangle",
|
||||
fill="#000000",
|
||||
line={}
|
||||
)
|
||||
assert elem.line == {}
|
||||
|
||||
def test_shape_all_shape_types(self):
|
||||
"""测试所有支持的形状类型"""
|
||||
shapes = ["rectangle", "ellipse", "rounded_rectangle"]
|
||||
for shape_type in shapes:
|
||||
elem = ShapeElement(
|
||||
box=[1, 1, 1, 1],
|
||||
shape=shape_type,
|
||||
fill="#000000"
|
||||
)
|
||||
assert elem.shape == shape_type
|
||||
|
||||
|
||||
class TestImageElementBoundaryCases:
|
||||
"""ImageElement 边界情况测试"""
|
||||
|
||||
def test_image_with_relative_path(self):
|
||||
"""测试相对路径"""
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 1, 1],
|
||||
src="images/test.png"
|
||||
)
|
||||
assert "images/test.png" == elem.src
|
||||
|
||||
def test_image_with_absolute_path(self):
|
||||
"""测试绝对路径"""
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 1, 1],
|
||||
src="/absolute/path/image.png"
|
||||
)
|
||||
assert elem.src == "/absolute/path/image.png"
|
||||
|
||||
def test_image_with_windows_path(self):
|
||||
"""测试 Windows 路径"""
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 1, 1],
|
||||
src="C:\\Images\\test.png"
|
||||
)
|
||||
assert elem.src == "C:\\Images\\test.png"
|
||||
|
||||
def test_image_with_special_characters_in_filename(self):
|
||||
"""测试文件名包含特殊字符"""
|
||||
special_names = [
|
||||
"image with spaces.png",
|
||||
"image-with-dashes.png",
|
||||
"image_with_underscores.png",
|
||||
"image.with.dots.png"
|
||||
]
|
||||
for name in special_names:
|
||||
elem = ImageElement(
|
||||
box=[0, 0, 1, 1],
|
||||
src=name
|
||||
)
|
||||
assert elem.src == name
|
||||
3
tests/unit/test_loaders/__init__.py
Normal file
3
tests/unit/test_loaders/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
加载器测试包
|
||||
"""
|
||||
171
tests/unit/test_loaders/test_yaml_loader.py
Normal file
171
tests/unit/test_loaders/test_yaml_loader.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
YAML 加载器单元测试
|
||||
|
||||
测试 load_yaml_file、validate_presentation_yaml 和 validate_template_yaml 函数
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from loaders.yaml_loader import (
|
||||
load_yaml_file, validate_presentation_yaml,
|
||||
validate_template_yaml, YAMLError
|
||||
)
|
||||
|
||||
|
||||
class TestLoadYamlFile:
|
||||
"""load_yaml_file 函数测试类"""
|
||||
|
||||
def test_load_valid_yaml(self, sample_yaml):
|
||||
"""测试加载有效的 YAML 文件"""
|
||||
data = load_yaml_file(sample_yaml)
|
||||
assert isinstance(data, dict)
|
||||
assert "slides" in data
|
||||
|
||||
def test_load_nonexistent_file(self, temp_dir):
|
||||
"""测试加载不存在的文件"""
|
||||
nonexistent = temp_dir / "nonexistent.yaml"
|
||||
with pytest.raises(YAMLError, match="文件不存在"):
|
||||
load_yaml_file(nonexistent)
|
||||
|
||||
def test_load_directory_raises_error(self, temp_dir):
|
||||
"""测试加载目录会引发错误"""
|
||||
with pytest.raises(YAMLError, match="不是有效的文件"):
|
||||
load_yaml_file(temp_dir)
|
||||
|
||||
def test_load_yaml_with_syntax_error(self, temp_dir):
|
||||
"""测试加载包含语法错误的 YAML"""
|
||||
invalid_file = temp_dir / "invalid.yaml"
|
||||
invalid_file.write_text("key: [unclosed list")
|
||||
with pytest.raises(YAMLError, match="YAML 语法错误"):
|
||||
load_yaml_file(invalid_file)
|
||||
|
||||
def test_load_utf8_content(self, temp_dir):
|
||||
"""测试加载 UTF-8 编码的内容"""
|
||||
utf8_file = temp_dir / "utf8.yaml"
|
||||
utf8_file.write_text("key: '中文内容'", encoding='utf-8')
|
||||
data = load_yaml_file(utf8_file)
|
||||
assert data["key"] == "中文内容"
|
||||
|
||||
|
||||
class TestValidatePresentationYaml:
|
||||
"""validate_presentation_yaml 函数测试类"""
|
||||
|
||||
def test_valid_presentation_structure(self):
|
||||
"""测试有效的演示文稿结构"""
|
||||
data = {
|
||||
"metadata": {"size": "16:9"},
|
||||
"slides": [{"elements": []}]
|
||||
}
|
||||
# 不应该引发错误
|
||||
validate_presentation_yaml(data)
|
||||
|
||||
def test_missing_slides_field(self):
|
||||
"""测试缺少 slides 字段"""
|
||||
data = {"metadata": {"size": "16:9"}}
|
||||
with pytest.raises(YAMLError, match="缺少必需字段 'slides'"):
|
||||
validate_presentation_yaml(data)
|
||||
|
||||
def test_slides_not_a_list(self):
|
||||
"""测试 slides 不是列表"""
|
||||
data = {"slides": "not a list"}
|
||||
with pytest.raises(YAMLError, match="'slides' 必须是一个列表"):
|
||||
validate_presentation_yaml(data)
|
||||
|
||||
def test_not_a_dict(self):
|
||||
"""测试不是字典"""
|
||||
data = "not a dict"
|
||||
with pytest.raises(YAMLError, match="必须是一个字典对象"):
|
||||
validate_presentation_yaml(data)
|
||||
|
||||
def test_empty_slides_list(self):
|
||||
"""测试空的 slides 列表"""
|
||||
data = {"slides": []}
|
||||
# 空列表是有效的
|
||||
validate_presentation_yaml(data)
|
||||
|
||||
def test_with_file_path_in_error(self):
|
||||
"""测试错误消息包含文件路径"""
|
||||
data = {"not_slides": []}
|
||||
with pytest.raises(YAMLError) as exc_info:
|
||||
validate_presentation_yaml(data, "test.yaml")
|
||||
assert "test.yaml" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestValidateTemplateYaml:
|
||||
"""validate_template_yaml 函数测试类"""
|
||||
|
||||
def test_valid_template_structure(self):
|
||||
"""测试有效的模板结构"""
|
||||
data = {
|
||||
"vars": [
|
||||
{"name": "title", "required": True}
|
||||
],
|
||||
"elements": []
|
||||
}
|
||||
# 不应该引发错误
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_missing_elements_field(self):
|
||||
"""测试缺少 elements 字段"""
|
||||
data = {"vars": []}
|
||||
with pytest.raises(YAMLError, match="缺少必需字段 'elements'"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_elements_not_a_list(self):
|
||||
"""测试 elements 不是列表"""
|
||||
data = {"elements": "not a list"}
|
||||
with pytest.raises(YAMLError, match="'elements' 必须是一个列表"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_vars_not_a_list(self):
|
||||
"""测试 vars 不是列表"""
|
||||
data = {
|
||||
"vars": "not a list",
|
||||
"elements": []
|
||||
}
|
||||
with pytest.raises(YAMLError, match="'vars' 必须是一个列表"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_var_item_not_a_dict(self):
|
||||
"""测试变量项不是字典"""
|
||||
data = {
|
||||
"vars": ["not a dict"],
|
||||
"elements": []
|
||||
}
|
||||
with pytest.raises(YAMLError, match="必须是一个字典对象"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_var_item_missing_name(self):
|
||||
"""测试变量项缺少 name 字段"""
|
||||
data = {
|
||||
"vars": [{"required": True}],
|
||||
"elements": []
|
||||
}
|
||||
with pytest.raises(YAMLError, match="缺少必需字段 'name'"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_template_without_vars(self):
|
||||
"""测试没有 vars 的模板"""
|
||||
data = {"elements": []}
|
||||
# vars 是可选的
|
||||
validate_template_yaml(data)
|
||||
|
||||
def test_not_a_dict(self):
|
||||
"""测试不是字典"""
|
||||
data = "not a dict"
|
||||
with pytest.raises(YAMLError, match="必须是一个字典对象"):
|
||||
validate_template_yaml(data)
|
||||
|
||||
|
||||
class TestYAMLError:
|
||||
"""YAMLError 异常测试类"""
|
||||
|
||||
def test_is_an_exception(self):
|
||||
"""测试 YAMLError 是异常类"""
|
||||
error = YAMLError("Test error")
|
||||
assert isinstance(error, Exception)
|
||||
|
||||
def test_can_be_raised_and_caught(self):
|
||||
"""测试可以被引发和捕获"""
|
||||
with pytest.raises(YAMLError):
|
||||
raise YAMLError("Test")
|
||||
333
tests/unit/test_presentation.py
Normal file
333
tests/unit/test_presentation.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Presentation 类单元测试
|
||||
|
||||
测试 Presentation 类的初始化、模板缓存和渲染方法
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, Mock
|
||||
from core.presentation import Presentation
|
||||
from loaders.yaml_loader import YAMLError
|
||||
|
||||
|
||||
class TestPresentationInit:
|
||||
"""Presentation 初始化测试类"""
|
||||
|
||||
def test_init_with_valid_yaml(self, sample_yaml):
|
||||
"""测试使用有效 YAML 初始化"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
assert pres.data is not None
|
||||
assert "slides" in pres.data
|
||||
assert pres.pres_file == sample_yaml
|
||||
|
||||
def test_init_with_templates_dir(self, sample_yaml, sample_template):
|
||||
"""测试带模板目录初始化"""
|
||||
pres = Presentation(str(sample_yaml), str(sample_template))
|
||||
|
||||
assert pres.templates_dir == sample_template
|
||||
assert isinstance(pres.template_cache, dict)
|
||||
|
||||
def test_init_without_templates_dir(self, sample_yaml):
|
||||
"""测试不带模板目录初始化"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
assert pres.templates_dir is None
|
||||
assert isinstance(pres.template_cache, dict)
|
||||
|
||||
@patch('core.presentation.load_yaml_file')
|
||||
@patch('core.presentation.validate_presentation_yaml')
|
||||
def test_init_loads_yaml(self, mock_validate, mock_load, temp_dir):
|
||||
"""测试初始化时加载 YAML"""
|
||||
mock_data = {"slides": [{"elements": []}]}
|
||||
mock_load.return_value = mock_data
|
||||
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
pres = Presentation(str(yaml_path))
|
||||
|
||||
mock_load.assert_called_once_with(str(yaml_path))
|
||||
mock_validate.assert_called_once()
|
||||
|
||||
def test_init_with_invalid_yaml(self, temp_dir):
|
||||
"""测试使用无效 YAML 初始化"""
|
||||
invalid_yaml = temp_dir / "invalid.yaml"
|
||||
invalid_yaml.write_text("invalid: [unclosed")
|
||||
|
||||
with pytest.raises(Exception):
|
||||
Presentation(str(invalid_yaml))
|
||||
|
||||
|
||||
class TestPresentationSize:
|
||||
"""Presentation size 属性测试类"""
|
||||
|
||||
def test_size_16_9(self, temp_dir):
|
||||
"""测试 16:9 尺寸"""
|
||||
yaml_content = """
|
||||
metadata:
|
||||
size: 16:9
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path))
|
||||
|
||||
assert pres.size == "16:9"
|
||||
|
||||
def test_size_4_3(self, temp_dir):
|
||||
"""测试 4:3 尺寸"""
|
||||
yaml_content = """
|
||||
metadata:
|
||||
size: 4:3
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path))
|
||||
|
||||
assert pres.size == "4:3"
|
||||
|
||||
def test_size_default(self, temp_dir):
|
||||
"""测试默认尺寸"""
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path))
|
||||
|
||||
assert pres.size == "16:9" # 默认值
|
||||
|
||||
def test_size_without_metadata(self, temp_dir):
|
||||
"""测试无 metadata 时的默认尺寸"""
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path))
|
||||
|
||||
assert pres.size == "16:9"
|
||||
|
||||
|
||||
class TestGetTemplate:
|
||||
"""get_template 方法测试类"""
|
||||
|
||||
@patch('core.presentation.Template')
|
||||
def test_get_template_caches_template(self, mock_template_class, sample_template):
|
||||
"""测试模板被缓存"""
|
||||
mock_template = Mock()
|
||||
mock_template_class.return_value = mock_template
|
||||
|
||||
# 创建一个使用模板的 YAML
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = sample_template / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path), str(sample_template))
|
||||
|
||||
# 第一次获取
|
||||
template1 = pres.get_template("test_template")
|
||||
# 第二次获取
|
||||
template2 = pres.get_template("test_template")
|
||||
|
||||
# 应该是同一个实例
|
||||
assert template1 is template2
|
||||
|
||||
@patch('core.presentation.Template')
|
||||
def test_get_template_creates_new_template(self, mock_template_class, sample_template):
|
||||
"""测试创建新模板"""
|
||||
mock_template = Mock()
|
||||
mock_template_class.return_value = mock_template
|
||||
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = sample_template / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path), str(sample_template))
|
||||
|
||||
# 获取模板
|
||||
template = pres.get_template("new_template")
|
||||
|
||||
# 应该创建模板
|
||||
mock_template_class.assert_called_once()
|
||||
assert template == mock_template
|
||||
|
||||
def test_get_template_without_templates_dir(self, sample_yaml):
|
||||
"""测试无模板目录时获取模板"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
# 应该在调用 Template 时失败,而不是 get_template
|
||||
with patch('core.presentation.Template') as mock_template_class:
|
||||
mock_template_class.side_effect = YAMLError("No template dir")
|
||||
|
||||
with pytest.raises(YAMLError):
|
||||
pres.get_template("test")
|
||||
|
||||
|
||||
class TestRenderSlide:
|
||||
"""render_slide 方法测试类"""
|
||||
|
||||
def test_render_slide_without_template(self, sample_yaml):
|
||||
"""测试渲染不使用模板的幻灯片"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"elements": [
|
||||
{"type": "text", "content": "Test", "box": [0, 0, 1, 1], "font": {}}
|
||||
]
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
assert "elements" in result
|
||||
assert len(result["elements"]) == 1
|
||||
assert result["elements"][0].content == "Test"
|
||||
|
||||
@patch('core.presentation.Template')
|
||||
def test_render_slide_with_template(self, mock_template_class, temp_dir, sample_template):
|
||||
"""测试渲染使用模板的幻灯片"""
|
||||
mock_template = Mock()
|
||||
mock_template.render.return_value = [
|
||||
{"type": "text", "content": "Template Title", "box": [0, 0, 1, 1], "font": {}}
|
||||
]
|
||||
mock_template_class.return_value = mock_template
|
||||
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path), str(sample_template))
|
||||
|
||||
slide_data = {
|
||||
"template": "title-slide",
|
||||
"vars": {"title": "My Title"}
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
# 模板应该被渲染
|
||||
mock_template.render.assert_called_once_with({"title": "My Title"})
|
||||
assert "elements" in result
|
||||
|
||||
def test_render_slide_with_background(self, sample_yaml):
|
||||
"""测试渲染带背景的幻灯片"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"background": {"color": "#ffffff"},
|
||||
"elements": []
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
assert result["background"] == {"color": "#ffffff"}
|
||||
|
||||
def test_render_slide_without_background(self, sample_yaml):
|
||||
"""测试渲染无背景的幻灯片"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"elements": []
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
assert result["background"] is None
|
||||
|
||||
@patch('core.presentation.create_element')
|
||||
def test_render_slide_converts_dict_to_objects(self, mock_create_element, sample_yaml):
|
||||
"""测试字典转换为元素对象"""
|
||||
mock_elem = Mock()
|
||||
mock_create_element.return_value = mock_elem
|
||||
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"elements": [
|
||||
{"type": "text", "content": "Test", "box": [0, 0, 1, 1], "font": {}}
|
||||
]
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
# create_element 应该被调用
|
||||
mock_create_element.assert_called()
|
||||
assert result["elements"][0] == mock_elem
|
||||
|
||||
def test_render_slide_with_template_merges_background(self, mock_template_class, temp_dir, sample_template):
|
||||
"""测试使用模板时合并背景"""
|
||||
mock_template = Mock()
|
||||
mock_template.render.return_value = [
|
||||
{"type": "text", "content": "Title", "box": [0, 0, 1, 1], "font": {}}
|
||||
]
|
||||
mock_template_class.return_value = mock_template
|
||||
|
||||
yaml_content = """
|
||||
slides:
|
||||
- elements: []
|
||||
"""
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
pres = Presentation(str(yaml_path), str(sample_template))
|
||||
|
||||
slide_data = {
|
||||
"template": "test",
|
||||
"vars": {},
|
||||
"background": {"color": "#ff0000"}
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
# 背景应该被保留
|
||||
assert result["background"] == {"color": "#ff0000"}
|
||||
|
||||
def test_render_slide_empty_elements_list(self, sample_yaml):
|
||||
"""测试空元素列表"""
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"elements": []
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
assert result["elements"] == []
|
||||
|
||||
@patch('core.presentation.create_element')
|
||||
def test_render_slide_with_multiple_elements(self, mock_create_element, sample_yaml):
|
||||
"""测试多个元素"""
|
||||
mock_elem1 = Mock()
|
||||
mock_elem2 = Mock()
|
||||
mock_create_element.side_effect = [mock_elem1, mock_elem2]
|
||||
|
||||
pres = Presentation(str(sample_yaml))
|
||||
|
||||
slide_data = {
|
||||
"elements": [
|
||||
{"type": "text", "content": "T1", "box": [0, 0, 1, 1], "font": {}},
|
||||
{"type": "text", "content": "T2", "box": [1, 1, 1, 1], "font": {}}
|
||||
]
|
||||
}
|
||||
|
||||
result = pres.render_slide(slide_data)
|
||||
|
||||
assert len(result["elements"]) == 2
|
||||
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)
|
||||
456
tests/unit/test_template.py
Normal file
456
tests/unit/test_template.py
Normal file
@@ -0,0 +1,456 @@
|
||||
"""
|
||||
模板系统单元测试
|
||||
|
||||
测试 Template 类的初始化、变量解析、条件渲染和模板渲染功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from loaders.yaml_loader import YAMLError
|
||||
from core.template import Template
|
||||
|
||||
|
||||
# ============= 模板初始化测试 =============
|
||||
|
||||
class TestTemplateInit:
|
||||
"""Template 初始化测试类"""
|
||||
|
||||
def test_init_without_template_dir_raises_error(self, temp_dir):
|
||||
"""测试未指定模板目录会引发错误"""
|
||||
with pytest.raises(YAMLError, match="未指定模板目录"):
|
||||
Template("title-slide", templates_dir=None)
|
||||
|
||||
def test_init_with_path_separator_in_name_raises_error(self, temp_dir):
|
||||
"""测试模板名称包含路径分隔符会引发错误"""
|
||||
with pytest.raises(YAMLError, match="模板名称不能包含路径分隔符"):
|
||||
Template("../etc/passwd", templates_dir=temp_dir)
|
||||
|
||||
def test_init_with_nonexistent_template_raises_error(self, temp_dir):
|
||||
"""测试模板文件不存在会引发错误"""
|
||||
with pytest.raises(YAMLError, match="模板文件不存在"):
|
||||
Template("nonexistent", templates_dir=temp_dir)
|
||||
|
||||
def test_init_with_valid_template(self, sample_template):
|
||||
"""测试使用有效模板初始化"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
assert template.data is not None
|
||||
assert "elements" in template.data
|
||||
assert "vars" in template.data
|
||||
|
||||
|
||||
# ============= 变量解析测试 =============
|
||||
|
||||
class TestResolveValue:
|
||||
"""resolve_value 方法测试类"""
|
||||
|
||||
def test_resolve_value_simple_variable(self, sample_template):
|
||||
"""测试解析简单变量"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.resolve_value("{title}", {"title": "My Title"})
|
||||
assert result == "My Title"
|
||||
|
||||
def test_resolve_value_multiple_variables(self, sample_template):
|
||||
"""测试解析多个变量"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.resolve_value("{title} - {subtitle}", {
|
||||
"title": "Main",
|
||||
"subtitle": "Sub"
|
||||
})
|
||||
assert result == "Main - Sub"
|
||||
|
||||
def test_resolve_value_undefined_variable_raises_error(self, sample_template):
|
||||
"""测试未定义变量会引发错误"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
with pytest.raises(YAMLError, match="未定义的变量"):
|
||||
template.resolve_value("{undefined}", {"title": "Test"})
|
||||
|
||||
def test_resolve_value_preserves_non_string(self):
|
||||
"""测试非字符串值保持原样"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
assert template.resolve_value(123, {}) == 123
|
||||
assert template.resolve_value(None, {}) is None
|
||||
assert template.resolve_value(["list"], {}) == ["list"]
|
||||
|
||||
def test_resolve_value_converts_to_integer(self, sample_template):
|
||||
"""测试结果转换为整数"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.resolve_value("{value}", {"value": "42"})
|
||||
assert result == 42
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_resolve_value_converts_to_float(self, sample_template):
|
||||
"""测试结果转换为浮点数"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.resolve_value("{value}", {"value": "3.14"})
|
||||
assert result == 3.14
|
||||
assert isinstance(result, float)
|
||||
|
||||
|
||||
# ============= resolve_element 测试 =============
|
||||
|
||||
class TestResolveElement:
|
||||
"""resolve_element 方法测试类"""
|
||||
|
||||
def test_resolve_element_dict(self, sample_template):
|
||||
"""测试解析字典元素"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
elem = {"content": "{title}", "box": [1, 2, 3, 4]}
|
||||
result = template.resolve_element(elem, {"title": "Test"})
|
||||
assert result["content"] == "Test"
|
||||
assert result["box"] == [1, 2, 3, 4]
|
||||
|
||||
def test_resolve_element_list(self, sample_template):
|
||||
"""测试解析列表元素"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
elem = ["{a}", "{b}", "static"]
|
||||
result = template.resolve_element(elem, {"a": "A", "b": "B"})
|
||||
assert result == ["A", "B", "static"]
|
||||
|
||||
def test_resolve_element_nested_structure(self, sample_template):
|
||||
"""测试解析嵌套结构"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
elem = {
|
||||
"font": {
|
||||
"size": "{size}",
|
||||
"color": "#000000"
|
||||
}
|
||||
}
|
||||
result = template.resolve_element(elem, {"size": "24"})
|
||||
assert result["font"]["size"] == 24
|
||||
assert result["font"]["color"] == "#000000"
|
||||
|
||||
def test_resolve_element_excludes_visible(self, sample_template):
|
||||
"""测试 visible 字段被排除"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
elem = {"content": "test", "visible": "{condition}"}
|
||||
result = template.resolve_element(elem, {})
|
||||
assert "visible" not in result
|
||||
|
||||
|
||||
# ============= 条件渲染测试 =============
|
||||
|
||||
class TestEvaluateCondition:
|
||||
"""evaluate_condition 方法测试类"""
|
||||
|
||||
def test_evaluate_condition_with_non_empty_variable(self, sample_template):
|
||||
"""测试非空变量条件为真"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.evaluate_condition("{subtitle != ''}", {
|
||||
"subtitle": "Test Subtitle"
|
||||
})
|
||||
assert result is True
|
||||
|
||||
def test_evaluate_condition_with_empty_variable(self, sample_template):
|
||||
"""测试空变量条件为假"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.evaluate_condition("{subtitle != ''}", {
|
||||
"subtitle": ""
|
||||
})
|
||||
assert result is False
|
||||
|
||||
def test_evaluate_condition_with_missing_variable(self, sample_template):
|
||||
"""测试缺失变量条件为假"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.evaluate_condition("{subtitle != ''}", {})
|
||||
assert result is False
|
||||
|
||||
def test_evaluate_condition_unrecognized_format_returns_true(self, sample_template):
|
||||
"""测试无法识别的条件格式默认返回 True"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.evaluate_condition("some other format", {})
|
||||
assert result is True
|
||||
|
||||
|
||||
# ============= 模板渲染测试 =============
|
||||
|
||||
class TestRender:
|
||||
"""render 方法测试类"""
|
||||
|
||||
def test_render_with_required_variable(self, sample_template):
|
||||
"""测试渲染包含必需变量的模板"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.render({"title": "My Presentation"})
|
||||
assert len(result) == 2 # 两个元素
|
||||
assert result[0]["content"] == "My Presentation"
|
||||
|
||||
def test_render_with_optional_variable(self, sample_template):
|
||||
"""测试渲染包含可选变量的模板"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.render({"title": "Test", "subtitle": "Subtitle"})
|
||||
assert len(result) == 2 # 两个元素
|
||||
|
||||
def test_render_without_optional_variable(self, sample_template):
|
||||
"""测试不提供可选变量"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
result = template.render({"title": "Test"})
|
||||
# subtitle 元素应该被跳过(visible 条件)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_render_missing_required_variable_raises_error(self, sample_template):
|
||||
"""测试缺少必需变量会引发错误"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
with pytest.raises(YAMLError, match="缺少必需变量"):
|
||||
template.render({}) # 缺少 title
|
||||
|
||||
def test_render_with_default_value(self, temp_dir):
|
||||
"""测试使用默认值"""
|
||||
# 创建带默认值的模板
|
||||
template_content = """
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: "Default Subtitle"
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "{title}"
|
||||
- type: text
|
||||
content: "{subtitle}"
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "default-test.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("default-test", templates_dir=temp_dir / "templates")
|
||||
result = template.render({"title": "Test"})
|
||||
assert len(result) == 2
|
||||
|
||||
def test_render_filters_elements_by_visible_condition(self, sample_template):
|
||||
"""测试根据 visible 条件过滤元素"""
|
||||
template = Template("title-slide", templates_dir=sample_template)
|
||||
|
||||
# 有 subtitle - 应该显示两个元素
|
||||
result_with = template.render({"title": "Test", "subtitle": "Sub"})
|
||||
assert len(result_with) == 2
|
||||
|
||||
# 无 subtitle - 应该只显示一个元素
|
||||
result_without = template.render({"title": "Test"})
|
||||
assert len(result_without) == 1
|
||||
|
||||
|
||||
# ============= 边界情况补充测试 =============
|
||||
|
||||
class TestTemplateBoundaryCases:
|
||||
"""模板系统边界情况测试"""
|
||||
|
||||
def test_nested_variable_resolution(self, temp_dir):
|
||||
"""测试嵌套变量解析"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: full_title
|
||||
required: false
|
||||
default: "{title} - Extended"
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "{full_title}"
|
||||
box: [0, 0, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "nested.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("nested", templates_dir=temp_dir / "templates")
|
||||
result = template.render({"title": "Main"})
|
||||
|
||||
# 默认值中的变量应该被解析
|
||||
assert result[0]["content"] == "Main - Extended"
|
||||
|
||||
def test_variable_with_special_characters(self, temp_dir):
|
||||
"""测试变量包含特殊字符"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "{title}"
|
||||
box: [0, 0, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "special.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("special", templates_dir=temp_dir / "templates")
|
||||
|
||||
special_values = [
|
||||
"Test & Data",
|
||||
"Test <Script>",
|
||||
"Test \"Quotes\"",
|
||||
"Test 'Apostrophe'",
|
||||
"测试中文",
|
||||
"Test: colon",
|
||||
"Test; semi"
|
||||
]
|
||||
for value in special_values:
|
||||
result = template.render({"title": value})
|
||||
assert result[0]["content"] == value
|
||||
|
||||
def test_variable_with_numeric_value(self, temp_dir):
|
||||
"""测试数字变量值"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: width
|
||||
required: true
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "Width: {width}"
|
||||
box: [0, 0, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "numeric.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("numeric", templates_dir=temp_dir / "templates")
|
||||
result = template.render({"width": "100"})
|
||||
|
||||
# 应该保持为字符串(因为是在 content 中)
|
||||
assert result[0]["content"] == "Width: 100"
|
||||
|
||||
def test_empty_vars_list(self, temp_dir):
|
||||
"""测试空的 vars 列表"""
|
||||
template_content = """
|
||||
vars: []
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "Static Content"
|
||||
box: [0, 0, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "novars.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("novars", templates_dir=temp_dir / "templates")
|
||||
result = template.render({})
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
def test_multiple_visible_conditions(self, temp_dir):
|
||||
"""测试多个可见性条件"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
- name: footer
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "{title}"
|
||||
box: [0, 0, 1, 1]
|
||||
font: {}
|
||||
|
||||
- type: text
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}"
|
||||
box: [0, 1, 1, 1]
|
||||
font: {}
|
||||
|
||||
- type: text
|
||||
content: "{footer}"
|
||||
visible: "{footer != ''}"
|
||||
box: [0, 2, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "multi-condition.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("multi-condition", templates_dir=temp_dir / "templates")
|
||||
|
||||
# 只有 title
|
||||
result1 = template.render({"title": "Test"})
|
||||
assert len(result1) == 1
|
||||
|
||||
# 有 subtitle 和 footer
|
||||
result2 = template.render({
|
||||
"title": "Test",
|
||||
"subtitle": "Sub",
|
||||
"footer": "Foot"
|
||||
})
|
||||
assert len(result2) == 3
|
||||
|
||||
def test_variable_in_position(self, temp_dir):
|
||||
"""测试变量在位置中使用"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: x_pos
|
||||
required: true
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "Test"
|
||||
box: ["{x_pos}", 1, 1, 1]
|
||||
font: {}
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "pos-var.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("pos-var", templates_dir=temp_dir / "templates")
|
||||
result = template.render({"x_pos": "2"})
|
||||
|
||||
# 变量应该被解析为数字
|
||||
assert result[0]["box"][0] == 2
|
||||
|
||||
def test_empty_template_elements(self, temp_dir):
|
||||
"""测试空元素列表的模板"""
|
||||
template_content = """
|
||||
vars: []
|
||||
|
||||
elements: []
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "empty.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("empty", templates_dir=temp_dir / "templates")
|
||||
result = template.render({})
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_variable_replacement_in_font(self, temp_dir):
|
||||
"""测试字体属性中的变量替换"""
|
||||
template_content = """
|
||||
vars:
|
||||
- name: font_size
|
||||
required: true
|
||||
- name: text_color
|
||||
required: true
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
content: "Styled Text"
|
||||
box: [0, 0, 1, 1]
|
||||
font:
|
||||
size: {font_size}
|
||||
color: {text_color}
|
||||
bold: true
|
||||
"""
|
||||
template_file = temp_dir / "templates" / "font-vars.yaml"
|
||||
template_file.parent.mkdir(exist_ok=True)
|
||||
template_file.write_text(template_content)
|
||||
|
||||
template = Template("font-vars", templates_dir=temp_dir / "templates")
|
||||
result = template.render({
|
||||
"font_size": "24",
|
||||
"text_color": "#ff0000"
|
||||
})
|
||||
|
||||
assert result[0]["font"]["size"] == 24
|
||||
assert result[0]["font"]["color"] == "#ff0000"
|
||||
104
tests/unit/test_utils.py
Normal file
104
tests/unit/test_utils.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
工具函数单元测试
|
||||
|
||||
测试颜色转换和格式验证函数
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from utils import hex_to_rgb, validate_color
|
||||
|
||||
|
||||
class TestHexToRgb:
|
||||
"""hex_to_rgb 函数测试类"""
|
||||
|
||||
def test_full_hex_format(self):
|
||||
"""测试完整的 #RRGGBB 格式"""
|
||||
assert hex_to_rgb("#ffffff") == (255, 255, 255)
|
||||
assert hex_to_rgb("#000000") == (0, 0, 0)
|
||||
assert hex_to_rgb("#4a90e2") == (74, 144, 226)
|
||||
assert hex_to_rgb("#FF0000") == (255, 0, 0)
|
||||
assert hex_to_rgb("#00FF00") == (0, 255, 0)
|
||||
assert hex_to_rgb("#0000FF") == (0, 0, 255)
|
||||
|
||||
def test_short_hex_format(self):
|
||||
"""测试短格式 #RGB"""
|
||||
assert hex_to_rgb("#fff") == (255, 255, 255)
|
||||
assert hex_to_rgb("#000") == (0, 0, 0)
|
||||
assert hex_to_rgb("#abc") == (170, 187, 204)
|
||||
assert hex_to_rgb("#ABC") == (170, 187, 204)
|
||||
|
||||
def test_without_hash_sign(self):
|
||||
"""测试没有 # 号"""
|
||||
assert hex_to_rgb("ffffff") == (255, 255, 255)
|
||||
assert hex_to_rgb("4a90e2") == (74, 144, 226)
|
||||
|
||||
def test_invalid_length(self):
|
||||
"""测试无效长度"""
|
||||
with pytest.raises(ValueError, match="无效的颜色格式"):
|
||||
hex_to_rgb("#ff") # 太短
|
||||
with pytest.raises(ValueError, match="无效的颜色格式"):
|
||||
hex_to_rgb("#fffff") # 5 位
|
||||
with pytest.raises(ValueError, match="无效的颜色格式"):
|
||||
hex_to_rgb("#fffffff") # 7 位
|
||||
|
||||
def test_invalid_characters(self):
|
||||
"""测试无效字符"""
|
||||
with pytest.raises(ValueError):
|
||||
hex_to_rgb("#gggggg")
|
||||
with pytest.raises(ValueError):
|
||||
hex_to_rgb("#xyz")
|
||||
|
||||
def test_mixed_case(self):
|
||||
"""测试大小写混合"""
|
||||
assert hex_to_rgb("#AbCdEf") == (171, 205, 239)
|
||||
assert hex_to_rgb("#aBcDeF") == (171, 205, 239)
|
||||
|
||||
|
||||
class TestValidateColor:
|
||||
"""validate_color 函数测试类"""
|
||||
|
||||
def test_valid_full_hex_colors(self):
|
||||
"""测试有效的完整十六进制颜色"""
|
||||
assert validate_color("#ffffff") is True
|
||||
assert validate_color("#000000") is True
|
||||
assert validate_color("#4a90e2") is True
|
||||
assert validate_color("#FF0000") is True
|
||||
assert validate_color("#ABCDEF") is True
|
||||
|
||||
def test_valid_short_hex_colors(self):
|
||||
"""测试有效的短格式十六进制颜色"""
|
||||
assert validate_color("#fff") is True
|
||||
assert validate_color("#000") is True
|
||||
assert validate_color("#abc") is True
|
||||
assert validate_color("#ABC") is True
|
||||
|
||||
def test_without_hash_sign(self):
|
||||
"""测试没有 # 号"""
|
||||
assert validate_color("ffffff") is False
|
||||
assert validate_color("fff") is False
|
||||
|
||||
def test_invalid_length(self):
|
||||
"""测试无效长度"""
|
||||
assert validate_color("#ff") is False
|
||||
assert validate_color("#ffff") is False
|
||||
assert validate_color("#fffff") is False
|
||||
assert validate_color("#fffffff") is False
|
||||
|
||||
def test_invalid_characters(self):
|
||||
"""测试无效字符"""
|
||||
assert validate_color("#gggggg") is False
|
||||
assert validate_color("#xyz") is False
|
||||
assert validate_color("red") is False
|
||||
assert validate_color("rgb(255,0,0)") is False
|
||||
|
||||
def test_non_string_input(self):
|
||||
"""测试非字符串输入"""
|
||||
assert validate_color(123) is False
|
||||
assert validate_color(None) is False
|
||||
assert validate_color([]) is False
|
||||
assert validate_color({"color": "#fff"}) is False
|
||||
|
||||
def test_empty_string(self):
|
||||
"""测试空字符串"""
|
||||
assert validate_color("") is False
|
||||
assert validate_color("#") is False
|
||||
3
tests/unit/test_validators/__init__.py
Normal file
3
tests/unit/test_validators/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
验证器测试包
|
||||
"""
|
||||
128
tests/unit/test_validators/test_geometry.py
Normal file
128
tests/unit/test_validators/test_geometry.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
几何验证器单元测试
|
||||
|
||||
测试 GeometryValidator 的元素边界检查功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from validators.geometry import GeometryValidator, TOLERANCE
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
|
||||
class TestGeometryValidator:
|
||||
"""GeometryValidator 测试类"""
|
||||
|
||||
def test_init(self):
|
||||
"""测试初始化"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
assert validator.slide_width == 10
|
||||
assert validator.slide_height == 5.625
|
||||
|
||||
def test_validate_element_within_bounds(self):
|
||||
"""测试元素在边界内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [1, 1, 5, 2]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_element_right_boundary_exceeded(self):
|
||||
"""测试元素右边界超出"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 3, 1] # right = 11 > 10
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_OUT_OF_BOUNDS"
|
||||
assert "右边界" in issues[0].message
|
||||
|
||||
def test_validate_element_bottom_boundary_exceeded(self):
|
||||
"""测试元素下边界超出"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [1, 5, 2, 1] # bottom = 6 > 5.625
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_OUT_OF_BOUNDS"
|
||||
assert "下边界" in issues[0].message
|
||||
|
||||
def test_validate_element_completely_out_of_bounds(self):
|
||||
"""测试元素完全在页面外"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [12, 7, 2, 1] # 完全在页面外
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_COMPLETELY_OUT_OF_BOUNDS"
|
||||
|
||||
def test_validate_element_within_tolerance(self):
|
||||
"""测试元素在容忍度范围内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
# 右边界 = 10.05,容忍度 0.1,应该通过
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 2.05, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_element_slightly_beyond_tolerance(self):
|
||||
"""测试元素稍微超出容忍度"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
# 右边界 = 10.15,超出容忍度 0.1
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 2.15, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
|
||||
def test_validate_table_within_bounds(self):
|
||||
"""测试表格在边界内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1],
|
||||
'col_widths': [2, 2, 2]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_table_exceeds_width(self):
|
||||
"""测试表格超出页面宽度"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1],
|
||||
'col_widths': [3, 3, 4] # 总宽 10,位置 1,右边界 11
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].code == "TABLE_OUT_OF_BOUNDS"
|
||||
|
||||
def test_validate_element_without_box(self):
|
||||
"""测试没有 box 属性的元素"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {})() # 没有 box 属性
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_table_without_col_widths(self):
|
||||
"""测试没有 col_widths 的表格"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
class TestToleranceConstant:
|
||||
"""容忍度常量测试"""
|
||||
|
||||
def test_tolerance_value(self):
|
||||
"""测试容忍度值为 0.1"""
|
||||
assert TOLERANCE == 0.1
|
||||
135
tests/unit/test_validators/test_resource.py
Normal file
135
tests/unit/test_validators/test_resource.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
资源验证器单元测试
|
||||
|
||||
测试 ResourceValidator 的图片和模板文件验证功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from validators.resource import ResourceValidator
|
||||
|
||||
|
||||
class TestResourceValidator:
|
||||
"""ResourceValidator 测试类"""
|
||||
|
||||
def test_init(self, temp_dir):
|
||||
"""测试初始化"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
assert validator.yaml_dir == temp_dir
|
||||
assert validator.template_dir is None
|
||||
|
||||
def test_init_with_template_dir(self, temp_dir):
|
||||
"""测试带模板目录初始化"""
|
||||
template_dir = temp_dir / "templates"
|
||||
validator = ResourceValidator(
|
||||
yaml_dir=temp_dir,
|
||||
template_dir=template_dir
|
||||
)
|
||||
assert validator.template_dir == template_dir
|
||||
|
||||
|
||||
class TestValidateImage:
|
||||
"""validate_image 方法测试"""
|
||||
|
||||
def test_existing_image(self, temp_dir, sample_image):
|
||||
"""测试存在的图片文件"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': sample_image.name})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_nonexistent_image(self, temp_dir):
|
||||
"""测试不存在的图片文件"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': 'nonexistent.png'})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "IMAGE_FILE_NOT_FOUND"
|
||||
|
||||
def test_image_with_absolute_path(self, temp_dir, sample_image):
|
||||
"""测试绝对路径图片"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': str(sample_image)})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_image_without_src_attribute(self, temp_dir):
|
||||
"""测试没有 src 属性的元素"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {})() # 没有 src 属性
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_image_with_empty_src(self, temp_dir):
|
||||
"""测试空 src 字符串"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': ''})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
class TestValidateTemplate:
|
||||
"""validate_template 方法测试"""
|
||||
|
||||
def test_slide_without_template(self, temp_dir):
|
||||
"""测试不使用模板的幻灯片"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
slide_data = {} # 没有 template 字段
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_template_with_dir_not_specified(self, temp_dir):
|
||||
"""测试使用模板但未指定模板目录"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=None)
|
||||
slide_data = {'template': 'title-slide'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_DIR_NOT_SPECIFIED"
|
||||
|
||||
def test_nonexistent_template_file(self, temp_dir):
|
||||
"""测试不存在的模板文件"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'nonexistent'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_FILE_NOT_FOUND"
|
||||
|
||||
def test_valid_template_file(self, sample_template):
|
||||
"""测试有效的模板文件"""
|
||||
validator = ResourceValidator(
|
||||
yaml_dir=sample_template.parent,
|
||||
template_dir=sample_template
|
||||
)
|
||||
slide_data = {'template': 'title-slide'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_template_with_yaml_extension(self, temp_dir):
|
||||
"""测试带 .yaml 扩展名的模板"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
(template_dir / "test.yaml").write_text("vars: []\nelements: []")
|
||||
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'test.yaml'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_invalid_template_structure(self, temp_dir):
|
||||
"""测试无效的模板结构"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
# 创建缺少 elements 字段的无效模板
|
||||
(template_dir / "invalid.yaml").write_text("vars: []")
|
||||
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'invalid'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_STRUCTURE_ERROR"
|
||||
155
tests/unit/test_validators/test_result.py
Normal file
155
tests/unit/test_validators/test_result.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
验证结果数据结构单元测试
|
||||
|
||||
测试 ValidationIssue 和 ValidationResult 类
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from validators.result import ValidationIssue, ValidationResult
|
||||
|
||||
|
||||
class TestValidationIssue:
|
||||
"""ValidationIssue 测试类"""
|
||||
|
||||
def test_create_error_issue(self):
|
||||
"""测试创建错误级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="ERROR",
|
||||
message="Test error",
|
||||
location="Slide 1",
|
||||
code="TEST_ERROR"
|
||||
)
|
||||
assert issue.level == "ERROR"
|
||||
assert issue.message == "Test error"
|
||||
assert issue.location == "Slide 1"
|
||||
assert issue.code == "TEST_ERROR"
|
||||
|
||||
def test_create_warning_issue(self):
|
||||
"""测试创建警告级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="WARNING",
|
||||
message="Test warning",
|
||||
location="",
|
||||
code="TEST_WARNING"
|
||||
)
|
||||
assert issue.level == "WARNING"
|
||||
|
||||
def test_create_info_issue(self):
|
||||
"""测试创建提示级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="INFO",
|
||||
message="Test info",
|
||||
location="",
|
||||
code="TEST_INFO"
|
||||
)
|
||||
assert issue.level == "INFO"
|
||||
|
||||
|
||||
class TestValidationResult:
|
||||
"""ValidationResult 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建"""
|
||||
result = ValidationResult(valid=True)
|
||||
assert result.valid is True
|
||||
assert result.errors == []
|
||||
assert result.warnings == []
|
||||
assert result.infos == []
|
||||
|
||||
def test_create_with_issues(self):
|
||||
"""测试创建包含问题的结果"""
|
||||
errors = [ValidationIssue("ERROR", "Error 1", "", "ERR1")]
|
||||
warnings = [ValidationIssue("WARNING", "Warning 1", "", "WARN1")]
|
||||
infos = [ValidationIssue("INFO", "Info 1", "", "INFO1")]
|
||||
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
infos=infos
|
||||
)
|
||||
assert result.valid is False
|
||||
assert len(result.errors) == 1
|
||||
assert len(result.warnings) == 1
|
||||
assert len(result.infos) == 1
|
||||
|
||||
def test_has_errors(self):
|
||||
"""测试 has_errors 方法"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")]
|
||||
)
|
||||
assert result.has_errors() is True
|
||||
|
||||
def test_has_errors_no_errors(self):
|
||||
"""测试没有错误时 has_errors 返回 False"""
|
||||
result = ValidationResult(valid=True)
|
||||
assert result.has_errors() is False
|
||||
|
||||
def test_format_output_clean(self):
|
||||
"""测试格式化输出 - 无问题"""
|
||||
result = ValidationResult(valid=True)
|
||||
output = result.format_output()
|
||||
assert "验证通过" in output
|
||||
|
||||
def test_format_output_with_errors(self):
|
||||
"""测试格式化输出 - 包含错误"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[
|
||||
ValidationIssue("ERROR", "Error 1", "Slide 1", "ERR1"),
|
||||
ValidationIssue("ERROR", "Error 2", "Slide 2", "ERR2")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "2 个错误" in output
|
||||
assert "Error 1" in output
|
||||
assert "Error 2" in output
|
||||
assert "[Slide 1]" in output
|
||||
|
||||
def test_format_output_with_warnings(self):
|
||||
"""测试格式化输出 - 包含警告"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
warnings=[
|
||||
ValidationIssue("WARNING", "Warning 1", "Slide 1", "WARN1")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个警告" in output
|
||||
assert "Warning 1" in output
|
||||
|
||||
def test_format_output_with_infos(self):
|
||||
"""测试格式化输出 - 包含提示"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
infos=[
|
||||
ValidationIssue("INFO", "Info 1", "", "INFO1")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个提示" in output
|
||||
|
||||
def test_format_output_mixed_issues(self):
|
||||
"""测试格式化输出 - 混合问题"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")],
|
||||
warnings=[ValidationIssue("WARNING", "Warning", "", "WARN")],
|
||||
infos=[ValidationIssue("INFO", "Info", "", "INFO")]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个错误" in output
|
||||
assert "1 个警告" in output
|
||||
assert "1 个提示" in output
|
||||
|
||||
def test_format_output_issue_without_location(self):
|
||||
"""测试没有位置信息的问题"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "Error" in output
|
||||
# 不应该有位置前缀
|
||||
assert "[]" not in output
|
||||
119
tests/unit/test_validators/test_validator.py
Normal file
119
tests/unit/test_validators/test_validator.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
主验证器单元测试
|
||||
|
||||
测试 Validator 类的协调和分类功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from validators.validator import Validator
|
||||
from validators.result import ValidationResult
|
||||
|
||||
|
||||
class TestValidator:
|
||||
"""Validator 测试类"""
|
||||
|
||||
def test_init(self):
|
||||
"""测试初始化"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES == {
|
||||
"16:9": (10, 5.625),
|
||||
"4:3": (10, 7.5)
|
||||
}
|
||||
|
||||
def test_validate_valid_yaml(self, sample_yaml):
|
||||
"""测试验证有效的 YAML 文件"""
|
||||
validator = Validator()
|
||||
result = validator.validate(sample_yaml)
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.valid is True
|
||||
|
||||
def test_validate_nonexistent_file(self, temp_dir):
|
||||
"""测试验证不存在的文件"""
|
||||
validator = Validator()
|
||||
nonexistent = temp_dir / "nonexistent.yaml"
|
||||
result = validator.validate(nonexistent)
|
||||
assert result.valid is False
|
||||
assert len(result.errors) > 0
|
||||
|
||||
def test_categorize_issues_error(self):
|
||||
"""测试分类问题为错误"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("ERROR", "Test", "", "ERR")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert len(warnings) == 0
|
||||
assert len(infos) == 0
|
||||
|
||||
def test_categorize_issues_warning(self):
|
||||
"""测试分类问题为警告"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("WARNING", "Test", "", "WARN")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 0
|
||||
assert len(warnings) == 1
|
||||
assert len(infos) == 0
|
||||
|
||||
def test_categorize_issues_info(self):
|
||||
"""测试分类问题为提示"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("INFO", "Test", "", "INFO")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 0
|
||||
assert len(warnings) == 0
|
||||
assert len(infos) == 1
|
||||
|
||||
def test_categorize_mixed_issues(self):
|
||||
"""测试分类混合问题"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issues = [
|
||||
ValidationIssue("ERROR", "Error", "", "ERR"),
|
||||
ValidationIssue("WARNING", "Warning", "", "WARN"),
|
||||
ValidationIssue("INFO", "Info", "", "INFO"),
|
||||
]
|
||||
validator._categorize_issues(issues, errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert len(warnings) == 1
|
||||
assert len(infos) == 1
|
||||
|
||||
|
||||
class TestSlideSizes:
|
||||
"""幻灯片尺寸常量测试"""
|
||||
|
||||
def test_16_9_size(self):
|
||||
"""测试 16:9 尺寸"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES["16:9"] == (10, 5.625)
|
||||
|
||||
def test_4_3_size(self):
|
||||
"""测试 4:3 尺寸"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES["4:3"] == (10, 7.5)
|
||||
Reference in New Issue
Block a user