1
0
Files
PPTX/tests/unit/test_presentation.py
lanyuanxiaoyao 2fd8bc1b4a feat: 为metadata、模板和幻灯片添加description字段支持
添加可选的description字段用于文档目的,不影响渲染输出。

主要更改:
- core/presentation.py: 添加metadata.description属性
- core/template.py: 添加template.description属性
- tests: 添加16个新测试用例验证description功能
- docs: 更新README.md和README_DEV.md文档
- specs: 新增page-description规范文件
2026-03-04 13:22:33 +08:00

761 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 == str(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
class TestRenderSlideHybridMode:
"""render_slide 混合模式测试类"""
@patch("core.presentation.Template")
def test_hybrid_mode_basic(self, mock_template_class, temp_dir, sample_template):
"""测试混合模式基本功能template + elements"""
mock_template = Mock()
mock_template.render.return_value = [
{"type": "text", "content": "From Template", "box": [0, 0, 1, 1], "font": {}}
]
mock_template.resolve_element.side_effect = lambda elem, vars: elem
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-template",
"vars": {"title": "Test"},
"elements": [
{"type": "text", "content": "Custom Element", "box": [2, 2, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# 应该有 2 个元素1 个来自模板1 个自定义
assert len(result["elements"]) == 2
assert result["elements"][0].content == "From Template"
assert result["elements"][1].content == "Custom Element"
@patch("core.presentation.Template")
def test_hybrid_mode_variable_sharing(self, mock_template_class, temp_dir, sample_template):
"""测试自定义元素访问模板变量"""
mock_template = Mock()
mock_template.render.return_value = []
# 模拟 resolve_element 解析变量
def resolve_element_mock(elem, vars):
resolved = elem.copy()
if "content" in resolved and "{" in resolved["content"]:
resolved["content"] = resolved["content"].replace("{theme_color}", vars.get("theme_color", ""))
if "fill" in resolved and "{" in resolved["fill"]:
resolved["fill"] = resolved["fill"].replace("{theme_color}", vars.get("theme_color", ""))
return resolved
mock_template.resolve_element.side_effect = resolve_element_mock
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-template",
"vars": {"theme_color": "#3949ab"},
"elements": [
{"type": "shape", "fill": "{theme_color}", "box": [0, 0, 1, 1]}
]
}
result = pres.render_slide(slide_data)
# 自定义元素应该使用模板变量
assert result["elements"][0].fill == "#3949ab"
def test_hybrid_mode_empty_elements(self, temp_dir, sample_template):
"""测试空 elements 列表"""
yaml_content = """
slides:
- elements: []
templates:
test-template:
vars:
- name: title
elements:
- type: text
content: "{title}"
box: [0, 0, 1, 1]
font: {}
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
slide_data = {
"template": "test-template",
"vars": {"title": "Test"},
"elements": []
}
result = pres.render_slide(slide_data)
# 空 elements 列表应该只渲染模板元素
assert len(result["elements"]) == 1
assert result["elements"][0].content == "Test"
def test_backward_compat_template_only(self, temp_dir, sample_template):
"""测试向后兼容:纯模板模式"""
yaml_content = """
slides:
- elements: []
templates:
test-template:
vars:
- name: title
elements:
- type: text
content: "{title}"
box: [0, 0, 1, 1]
font: {}
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
slide_data = {
"template": "test-template",
"vars": {"title": "Test"}
}
result = pres.render_slide(slide_data)
# 应该只有模板元素
assert len(result["elements"]) == 1
assert result["elements"][0].content == "Test"
def test_backward_compat_custom_only(self, sample_yaml):
"""测试向后兼容:纯自定义元素模式"""
pres = Presentation(str(sample_yaml))
slide_data = {
"elements": [
{"type": "text", "content": "Custom", "box": [0, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# 应该只有自定义元素
assert len(result["elements"]) == 1
assert result["elements"][0].content == "Custom"
def test_hybrid_mode_with_inline_template(self, temp_dir):
"""测试内联模板与自定义元素混合使用"""
yaml_content = """
slides:
- elements: []
templates:
test-template:
vars:
- name: title
elements:
- type: text
content: "{title}"
box: [0, 0, 1, 1]
font: {}
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
slide_data = {
"template": "test-template",
"vars": {"title": "Inline Template"},
"elements": [
{"type": "text", "content": "Custom", "box": [2, 2, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# 应该有 2 个元素
assert len(result["elements"]) == 2
assert result["elements"][0].content == "Inline Template"
assert result["elements"][1].content == "Custom"
@patch("core.presentation.Template")
def test_hybrid_mode_with_external_template(self, mock_template_class, temp_dir, sample_template):
"""测试外部模板与自定义元素混合使用"""
mock_template = Mock()
mock_template.render.return_value = [
{"type": "text", "content": "External", "box": [0, 0, 1, 1], "font": {}}
]
mock_template.resolve_element.side_effect = lambda elem, vars: elem
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": "external-template",
"vars": {},
"elements": [
{"type": "text", "content": "Custom", "box": [2, 2, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# 应该有 2 个元素
assert len(result["elements"]) == 2
assert result["elements"][0].content == "External"
assert result["elements"][1].content == "Custom"
@patch("core.presentation.Template")
def test_hybrid_mode_element_order(self, mock_template_class, temp_dir, sample_template):
"""测试元素顺序:模板元素在前,自定义元素在后"""
mock_template = Mock()
mock_template.render.return_value = [
{"type": "text", "content": "Template1", "box": [0, 0, 1, 1], "font": {}},
{"type": "text", "content": "Template2", "box": [1, 0, 1, 1], "font": {}}
]
mock_template.resolve_element.side_effect = lambda elem, vars: elem
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-template",
"vars": {},
"elements": [
{"type": "text", "content": "Custom1", "box": [2, 0, 1, 1], "font": {}},
{"type": "text", "content": "Custom2", "box": [3, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# 验证顺序:模板元素在前
assert len(result["elements"]) == 4
assert result["elements"][0].content == "Template1"
assert result["elements"][1].content == "Template2"
assert result["elements"][2].content == "Custom1"
assert result["elements"][3].content == "Custom2"
# ============= Description 字段测试 =============
class TestPresentationDescription:
"""Presentation description 字段测试类"""
def test_metadata_with_description(self, temp_dir):
"""测试 metadata 包含 description 字段时正确加载"""
yaml_content = """
metadata:
title: "测试演示文稿"
description: "这是关于项目年度总结的演示文稿"
size: "16:9"
slides:
- elements: []
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
assert pres.description == "这是关于项目年度总结的演示文稿"
def test_metadata_without_description(self, temp_dir):
"""测试 metadata 不包含 description 字段时正常工作"""
yaml_content = """
metadata:
title: "测试演示文稿"
size: "16:9"
slides:
- elements: []
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
assert pres.description is None
def test_metadata_description_empty_string(self, temp_dir):
"""测试 metadata description 为空字符串时正常工作"""
yaml_content = """
metadata:
title: "测试演示文稿"
description: ""
size: "16:9"
slides:
- elements: []
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
assert pres.description == ""
def test_metadata_description_chinese_characters(self, temp_dir):
"""测试 metadata description 包含中文字符时正确处理"""
yaml_content = """
metadata:
title: "测试"
description: "这是关于项目的演示文稿,包含中文和特殊字符:测试、验证、确认"
size: "16:9"
slides:
- elements: []
"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(yaml_content)
pres = Presentation(str(yaml_path))
assert "这是关于项目的演示文稿" in pres.description
assert "中文" in pres.description
class TestSlideDescription:
"""Slide description 字段测试类"""
def test_slide_with_description(self, sample_yaml):
"""测试幻灯片包含 description 字段时正确加载"""
pres = Presentation(str(sample_yaml))
slide_data = {
"description": "介绍项目背景和目标",
"elements": [
{"type": "text", "content": "Test", "box": [0, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
assert result["description"] == "介绍项目背景和目标"
def test_slide_without_description(self, sample_yaml):
"""测试幻灯片不包含 description 字段时正常工作"""
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 result["description"] is None
def test_slide_description_empty_string(self, sample_yaml):
"""测试幻灯片 description 为空字符串时正常工作"""
pres = Presentation(str(sample_yaml))
slide_data = {
"description": "",
"elements": [
{"type": "text", "content": "Test", "box": [0, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
assert result["description"] == ""
def test_slide_description_chinese_characters(self, sample_yaml):
"""测试幻灯片 description 包含中文字符时正确处理"""
pres = Presentation(str(sample_yaml))
slide_data = {
"description": "这是幻灯片描述,包含中文内容",
"elements": [
{"type": "text", "content": "Test", "box": [0, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
assert "这是幻灯片描述" in result["description"]
assert "中文" in result["description"]
def test_slide_description_does_not_affect_rendering(self, sample_yaml):
"""测试 description 不影响渲染输出"""
pres = Presentation(str(sample_yaml))
slide_data = {
"description": "这段描述不应该影响渲染",
"elements": [
{"type": "text", "content": "Test Content", "box": [0, 0, 1, 1], "font": {}}
]
}
result = pres.render_slide(slide_data)
# description 字段存在但不影响元素渲染
assert result["description"] == "这段描述不应该影响渲染"
assert len(result["elements"]) == 1
assert result["elements"][0].content == "Test Content"