移除图片 fit 和 background 参数支持,简化图片渲染逻辑。系统恢复到直接使用 python-pptx 原生图片添加功能,图片将被拉伸到指定尺寸。 变更内容: - 移除 ImageElement 的 fit 和 background 字段 - 移除 metadata.dpi 配置 - 删除 utils/image_utils.py 图片处理工具模块 - 删除 validators/image_config.py 验证器 - 简化 PPTX 和 HTML 渲染器的图片处理逻辑 - HTML 渲染器使用硬编码 DPI=96(Web 标准) - 删除相关测试文件(单元测试、集成测试、e2e 测试) - 更新规格文档和用户文档 - 保留 Pillow 依赖用于未来可能的图片处理需求 影响: - 删除 11 个文件 - 修改 10 个文件 - 净减少 1558 行代码 - 所有 402 个测试通过 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
467 lines
15 KiB
Python
467 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]
|
|
|
|
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
|