1
0
Files
PPTX/tests/unit/test_elements.py
lanyuanxiaoyao 19d6661381 feat: 添加图片适配模式支持
- 支持四种图片适配模式:stretch、contain、cover、center
- 支持背景色填充功能(contain 和 center 模式)
- 支持文档级 DPI 配置(metadata.dpi)
- PPTX 渲染器集成 Pillow 实现高质量图片处理
- HTML 渲染器使用 CSS object-fit 实现相同效果
- 添加完整的单元测试、集成测试和端到端测试
- 更新 README 文档和架构文档
- 模块化设计:utils/image_utils.py 图片处理工具模块
- 添加图片配置验证器:validators/image_config.py
- 向后兼容:未指定 fit 时默认使用 stretch 模式
2026-03-04 10:29:21 +08:00

480 lines
15 KiB
Python

"""
元素类单元测试
测试 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]
assert elem.fit is None
assert elem.background is None
def test_create_with_fit_and_background(self):
"""测试创建带 fit 和 background 的 ImageElement"""
elem = ImageElement(
src="test.png",
box=[1, 1, 4, 3],
fit="contain",
background="#ffffff"
)
assert elem.fit == "contain"
assert elem.background == "#ffffff"
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