""" 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"