添加可选的description字段用于文档目的,不影响渲染输出。 主要更改: - core/presentation.py: 添加metadata.description属性 - core/template.py: 添加template.description属性 - tests: 添加16个新测试用例验证description功能 - docs: 更新README.md和README_DEV.md文档 - specs: 新增page-description规范文件
1058 lines
33 KiB
Python
1058 lines
33 KiB
Python
"""
|
||
模板系统单元测试
|
||
|
||
测试 Template 类的初始化、变量解析、条件渲染和模板渲染功能
|
||
"""
|
||
|
||
import pytest
|
||
from pathlib import Path
|
||
from loaders.yaml_loader import YAMLError
|
||
from core.template import Template
|
||
from core.presentation import Presentation
|
||
|
||
|
||
# ============= 模板初始化测试 =============
|
||
|
||
|
||
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, sample_template):
|
||
"""测试非字符串值保持原样"""
|
||
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)
|
||
with pytest.raises(YAMLError, match="条件表达式中的变量未定义"):
|
||
template.evaluate_condition("{subtitle != ''}", {})
|
||
|
||
def test_evaluate_condition_complex_logic(self, sample_template):
|
||
"""测试复杂逻辑表达式"""
|
||
template = Template("title-slide", templates_dir=sample_template)
|
||
result = template.evaluate_condition(
|
||
"{count > 0 and status == 'active'}",
|
||
{"count": 5, "status": "active"}
|
||
)
|
||
assert result is True
|
||
|
||
def test_evaluate_condition_member_test(self, sample_template):
|
||
"""测试成员测试表达式"""
|
||
template = Template("title-slide", templates_dir=sample_template)
|
||
result = template.evaluate_condition(
|
||
"{status in ['draft', 'review']}",
|
||
{"status": "draft"}
|
||
)
|
||
assert result is True
|
||
|
||
def test_evaluate_condition_math_operation(self, sample_template):
|
||
"""测试数学运算表达式"""
|
||
template = Template("title-slide", templates_dir=sample_template)
|
||
result = template.evaluate_condition(
|
||
"{(price * discount) > 50}",
|
||
{"price": 100, "discount": 0.8}
|
||
)
|
||
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"})
|
||
# 由于条件渲染,subtitle元素被跳过,只返回1个元素
|
||
assert len(result) == 1
|
||
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"
|
||
assert result[0]["font"]["color"] == "#ff0000"
|
||
|
||
|
||
# ============= Template.from_data() 方法测试 =============
|
||
|
||
class TestTemplateFromData:
|
||
"""Template.from_data() 类方法测试类"""
|
||
|
||
def test_from_data_with_valid_template(self):
|
||
"""测试从字典创建模板"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'title', 'required': True},
|
||
{'name': 'subtitle', 'required': False, 'default': ''}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{title}'},
|
||
{'type': 'text', 'box': [1, 3.5, 8, 0.5], 'content': '{subtitle}', 'visible': "{subtitle != ''}"}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
|
||
assert template.data == template_data
|
||
assert template.vars_def == {'title': {'name': 'title', 'required': True}, 'subtitle': {'name': 'subtitle', 'required': False, 'default': ''}}
|
||
assert template.elements == template_data['elements']
|
||
assert len(template.elements) == 2
|
||
|
||
def test_from_data_without_vars(self):
|
||
"""测试没有 vars 的模板"""
|
||
template_data = {
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': 'Static Content'}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'no-vars-template')
|
||
|
||
assert template.vars_def == {}
|
||
assert len(template.elements) == 1
|
||
|
||
def test_from_data_with_empty_vars(self):
|
||
"""测试 vars 为空列表的模板"""
|
||
template_data = {
|
||
'vars': [],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': 'Static'}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'empty-vars-template')
|
||
|
||
assert template.vars_def == {}
|
||
|
||
def test_from_data_render_with_vars(self):
|
||
"""测试 from_data 创建的模板可以正常渲染"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'title', 'required': True}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{title}'}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
result = template.render({'title': 'My Title'})
|
||
|
||
assert len(result) == 1
|
||
assert result[0]['content'] == 'My Title'
|
||
|
||
def test_from_data_render_with_default_value(self):
|
||
"""测试 from_data 创建的模板支持默认值"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'title', 'required': True},
|
||
{'name': 'subtitle', 'required': False, 'default': 'Default Subtitle'}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{title}'},
|
||
{'type': 'text', 'box': [1, 3.5, 8, 0.5], 'content': '{subtitle}', 'visible': "{subtitle != ''}"}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
|
||
# 不提供 subtitle,应该使用默认值
|
||
result = template.render({'title': 'My Title'})
|
||
|
||
assert len(result) == 2
|
||
assert result[1]['content'] == 'Default Subtitle'
|
||
|
||
def test_from_data_with_conditional_element(self):
|
||
"""测试 from_data 创建的模板支持条件渲染"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'subtitle', 'required': False, 'default': ''}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{title}'},
|
||
{'type': 'text', 'box': [1, 3.5, 8, 0.5], 'content': '{subtitle}', 'visible': "{subtitle != ''}"}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
|
||
# 不提供 subtitle,元素应该被过滤
|
||
result = template.render({'title': 'Main', 'subtitle': ''})
|
||
|
||
assert len(result) == 1
|
||
assert result[0]['content'] == 'Main'
|
||
|
||
# 提供 subtitle,元素应该被显示
|
||
result = template.render({'title': 'Main', 'subtitle': 'Subtitle'})
|
||
assert len(result) == 2
|
||
|
||
def test_from_data_with_nested_variable_in_default(self):
|
||
"""测试 from_data 创建的模板支持嵌套变量默认值"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'title', 'required': True},
|
||
{'name': 'full_title', 'required': False, 'default': '{title} - Extended'}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{full_title}'}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
result = template.render({'title': 'Main'})
|
||
|
||
# 默认值中的 {title} 应该被替换
|
||
assert result[0]['content'] == 'Main - Extended'
|
||
|
||
def test_from_data_rejects_template_reference_in_elements(self):
|
||
"""测试内联模板元素中引用其他模板应该被拒绝"""
|
||
template_data = {
|
||
'vars': [
|
||
{'name': 'title', 'required': True}
|
||
],
|
||
'elements': [
|
||
{'type': 'text', 'box': [1, 2, 8, 1], 'content': '{title}', 'template': 'other-template'}
|
||
]
|
||
}
|
||
template = Template.from_data(template_data, 'test-template')
|
||
|
||
# 渲染时应该抛出错误
|
||
with pytest.raises(YAMLError, match="内联模板不支持相互引用"):
|
||
template.render({'title': 'My Title'})
|
||
|
||
class TestInlineTemplates:
|
||
"""内联模板功能集成测试类"""
|
||
|
||
def test_presentation_with_inline_templates(self, temp_dir):
|
||
"""测试演示文稿包含内联模板定义"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
my-template:
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
elements:
|
||
- type: text
|
||
box: [1, 2, 8, 1]
|
||
content: "{title}"
|
||
|
||
slides:
|
||
- template: my-template
|
||
vars:
|
||
title: "内联模板测试"
|
||
'''
|
||
yaml_file = temp_dir / "inline_test.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from core.presentation import Presentation
|
||
pres = Presentation(str(yaml_file))
|
||
|
||
assert 'my-template' in pres.inline_templates
|
||
assert len(pres.inline_templates) == 1
|
||
|
||
def test_presentation_without_inline_templates(self, temp_dir):
|
||
"""测试演示文稿不包含内联模板定义"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
slides:
|
||
- elements:
|
||
- type: text
|
||
box: [1, 2, 8, 1]
|
||
content: "直接元素"
|
||
'''
|
||
yaml_file = temp_dir / "no_inline_test.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from core.presentation import Presentation
|
||
pres = Presentation(str(yaml_file))
|
||
|
||
assert hasattr(pres, 'inline_templates')
|
||
assert pres.inline_templates == {}
|
||
|
||
class TestInlineTemplateNameConflict:
|
||
"""内联和外部模板同名冲突测试类"""
|
||
|
||
def test_inline_and_external_template_same_name_raises_error(self, temp_dir):
|
||
"""测试内联和外部模板同名时抛出 ERROR"""
|
||
from core.presentation import Presentation
|
||
|
||
# 创建外部模板
|
||
templates_dir = temp_dir / "templates"
|
||
templates_dir.mkdir(exist_ok=True)
|
||
|
||
external_template_content = '''vars:
|
||
- name: title
|
||
required: true
|
||
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
'''
|
||
(templates_dir / "conflict-template.yaml").write_text(external_template_content, encoding='utf-8')
|
||
|
||
# 创建演示文稿 YAML(包含同名内联模板)
|
||
yaml_content = '''metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
conflict-template:
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
|
||
slides:
|
||
- template: conflict-template
|
||
vars:
|
||
title: "测试"
|
||
'''
|
||
yaml_file = temp_dir / "test.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
# 应该抛出 YAMLError
|
||
with pytest.raises(YAMLError, match="模板名称冲突"):
|
||
pres = Presentation(str(yaml_file), templates_dir=str(templates_dir))
|
||
# 手动调用 get_template 来触发冲突检测
|
||
pres.get_template('conflict-template')
|
||
|
||
def test_inline_only_no_conflict(self, temp_dir):
|
||
"""测试只有内联模板时正常工作"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
my-template:
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
|
||
slides:
|
||
- template: my-template
|
||
vars:
|
||
title: "测试"
|
||
'''
|
||
yaml_file = temp_dir / "inline_only.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from core.presentation import Presentation
|
||
pres = Presentation(str(yaml_file))
|
||
|
||
# 不提供外部模板目录,应该正常加载
|
||
assert 'my-template' in pres.inline_templates
|
||
slide_data = pres.data['slides'][0]
|
||
rendered = pres.render_slide(slide_data)
|
||
assert len(rendered['elements']) == 1
|
||
|
||
class TestInlineTemplateErrorHandling:
|
||
"""内联模板错误处理测试类"""
|
||
|
||
def test_templates_not_a_dict_raises_error(self, temp_dir):
|
||
"""测试 templates 字段不是字典时抛出错误"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates: "invalid_templates_value"
|
||
|
||
slides:
|
||
- elements:
|
||
- type: text
|
||
content: "test"
|
||
'''
|
||
yaml_file = temp_dir / "invalid_templates.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from loaders.yaml_loader import YAMLError
|
||
from core.presentation import Presentation
|
||
|
||
with pytest.raises(YAMLError, match="'templates' 必须是一个字典"):
|
||
Presentation(str(yaml_file))
|
||
|
||
def test_template_missing_elements_raises_error(self, temp_dir):
|
||
"""测试模板缺少 elements 字段时抛出错误"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
no-elements:
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
|
||
slides:
|
||
- elements:
|
||
- type: text
|
||
content: "test"
|
||
'''
|
||
yaml_file = temp_dir / "no_elements.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from loaders.yaml_loader import YAMLError
|
||
from core.presentation import Presentation
|
||
|
||
with pytest.raises(YAMLError, match="缺少必需字段 'elements'"):
|
||
Presentation(str(yaml_file))
|
||
|
||
def test_template_var_missing_name_raises_error(self, temp_dir):
|
||
"""测试变量定义缺少 name 字段时抛出错误"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
test-template:
|
||
vars:
|
||
- required: true
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
|
||
slides:
|
||
- template: test-template
|
||
vars:
|
||
title: "Test"
|
||
'''
|
||
yaml_file = temp_dir / "invalid_var.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from loaders.yaml_loader import YAMLError
|
||
from core.presentation import Presentation
|
||
|
||
with pytest.raises(YAMLError, match="缺少必需字段 'name'"):
|
||
Presentation(str(yaml_file))
|
||
|
||
def test_missing_required_variable_raises_error(self, temp_dir):
|
||
"""测试缺少必需变量时抛出错误"""
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
title-slide:
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
|
||
slides:
|
||
- template: title-slide
|
||
vars:
|
||
# 缺少必需的 title 变量
|
||
subtitle: "Test"
|
||
'''
|
||
yaml_file = temp_dir / "missing_var.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from core.presentation import Presentation
|
||
pres = Presentation(str(yaml_file))
|
||
slide_data = pres.data['slides'][0]
|
||
|
||
# 渲染时应该抛出错误
|
||
with pytest.raises(YAMLError, match="缺少必需变量: title"):
|
||
pres.render_slide(slide_data)
|
||
|
||
def test_backward_compatibility_without_templates_field(self, temp_dir):
|
||
"""测试不使用 templates 字段时保持向后兼容"""
|
||
# 复用现有的 sample_yaml fixture
|
||
yaml_content = '''
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
slides:
|
||
- background:
|
||
color: "#ffffff"
|
||
elements:
|
||
- type: text
|
||
box: [1, 1, 8, 1]
|
||
content: "Hello, World!"
|
||
font:
|
||
size: 44
|
||
bold: true
|
||
color: "#333333"
|
||
align: center
|
||
'''
|
||
yaml_file = temp_dir / "backward_test.yaml"
|
||
yaml_file.write_text(yaml_content, encoding='utf-8')
|
||
|
||
from core.presentation import Presentation
|
||
|
||
# 不使用 templates 字段,应该保持现有行为
|
||
pres = Presentation(str(yaml_file))
|
||
|
||
# 不应该有 inline_templates 属性(因为字段不存在)
|
||
assert not hasattr(pres, 'inline_templates') or pres.inline_templates == {}
|
||
|
||
# 渲染应该正常工作
|
||
slide_data = pres.data['slides'][0]
|
||
rendered = pres.render_slide(slide_data)
|
||
assert len(rendered['elements']) == 1
|
||
|
||
|
||
# ============= Description 字段测试 =============
|
||
|
||
|
||
class TestTemplateDescription:
|
||
"""Template description 字段测试类"""
|
||
|
||
def test_template_with_description(self, temp_dir):
|
||
"""测试模板包含 description 字段时正确加载"""
|
||
template_content = """
|
||
description: "用于章节标题页的模板,包含主标题和副标题"
|
||
vars:
|
||
- name: title
|
||
required: true
|
||
- name: subtitle
|
||
required: false
|
||
default: ""
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font:
|
||
size: 44
|
||
bold: true
|
||
- type: text
|
||
content: "{subtitle}"
|
||
box: [1, 2, 8, 1]
|
||
font:
|
||
size: 24
|
||
"""
|
||
template_path = temp_dir / "test-template.yaml"
|
||
template_path.write_text(template_content)
|
||
|
||
template = Template("test-template", templates_dir=temp_dir)
|
||
|
||
assert template.description == "用于章节标题页的模板,包含主标题和副标题"
|
||
|
||
def test_template_without_description(self, sample_template):
|
||
"""测试模板不包含 description 字段时正常工作"""
|
||
template = Template("title-slide", templates_dir=sample_template)
|
||
|
||
assert template.description is None
|
||
|
||
def test_template_description_empty_string(self, temp_dir):
|
||
"""测试模板 description 为空字符串时正常工作"""
|
||
template_content = """
|
||
description: ""
|
||
vars:
|
||
- name: title
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font: {}
|
||
"""
|
||
template_path = temp_dir / "test-template.yaml"
|
||
template_path.write_text(template_content)
|
||
|
||
template = Template("test-template", templates_dir=temp_dir)
|
||
|
||
assert template.description == ""
|
||
|
||
def test_template_description_chinese_characters(self, temp_dir):
|
||
"""测试模板 description 包含中文字符时正确处理"""
|
||
template_content = """
|
||
description: "这是中文模板描述,用于标题页面"
|
||
vars:
|
||
- name: title
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font: {}
|
||
"""
|
||
template_path = temp_dir / "test-template.yaml"
|
||
template_path.write_text(template_content)
|
||
|
||
template = Template("test-template", templates_dir=temp_dir)
|
||
|
||
assert "这是中文模板描述" in template.description
|
||
assert "标题页面" in template.description
|
||
|
||
def test_template_description_multiline(self, temp_dir):
|
||
"""测试模板 description 支持多行文本"""
|
||
template_content = """
|
||
description: |
|
||
这是一个多行描述。
|
||
第一行说明模板的用途。
|
||
第二行说明使用场景。
|
||
vars:
|
||
- name: title
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font: {}
|
||
"""
|
||
template_path = temp_dir / "test-template.yaml"
|
||
template_path.write_text(template_content)
|
||
|
||
template = Template("test-template", templates_dir=temp_dir)
|
||
|
||
# 多行文本应该被正确读取
|
||
assert "这是一个多行描述" in template.description
|
||
assert "第一行说明模板的用途" in template.description
|
||
assert "第二行说明使用场景" in template.description
|
||
|
||
def test_inline_template_with_description(self, temp_dir):
|
||
"""测试内联模板包含 description 字段"""
|
||
yaml_content = """
|
||
metadata:
|
||
size: "16:9"
|
||
|
||
templates:
|
||
test-template:
|
||
description: "内联模板描述"
|
||
vars:
|
||
- name: title
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font: {}
|
||
|
||
slides:
|
||
- template: test-template
|
||
vars:
|
||
title: "Test"
|
||
"""
|
||
yaml_path = temp_dir / "test.yaml"
|
||
yaml_path.write_text(yaml_content)
|
||
|
||
pres = Presentation(str(yaml_path))
|
||
template = pres.get_template("test-template")
|
||
|
||
assert template.description == "内联模板描述"
|
||
|
||
def test_template_description_does_not_affect_rendering(self, temp_dir):
|
||
"""测试 description 不影响模板渲染"""
|
||
template_content = """
|
||
description: "这段描述不应该影响渲染"
|
||
vars:
|
||
- name: title
|
||
elements:
|
||
- type: text
|
||
content: "{title}"
|
||
box: [1, 1, 8, 1]
|
||
font:
|
||
size: 44
|
||
"""
|
||
template_path = temp_dir / "test-template.yaml"
|
||
template_path.write_text(template_content)
|
||
|
||
template = Template("test-template", templates_dir=temp_dir)
|
||
|
||
# 渲染应该正常工作,description 不影响结果
|
||
result = template.render({"title": "Test Title"})
|
||
|
||
assert len(result) == 1
|
||
assert result[0]["content"] == "Test Title"
|
||
|