""" PPTX 渲染器单元测试 测试 PptxGenerator 类的初始化和渲染方法 """ import pytest from pathlib import Path from unittest.mock import Mock, MagicMock, patch from renderers.pptx_renderer import PptxGenerator from loaders.yaml_loader import YAMLError from core.elements import TextElement, ImageElement, ShapeElement, TableElement class TestPptxGeneratorInit: """PptxGenerator 初始化测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_init_with_16_9_size(self, mock_prs_class): """测试使用 16:9 尺寸初始化""" mock_prs = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator(size="16:9") assert gen.prs == mock_prs # 验证属性被设置(具体值由 Inches 决定) assert hasattr(mock_prs, "slide_width") assert hasattr(mock_prs, "slide_height") @patch("renderers.pptx_renderer.PptxPresentation") def test_init_with_4_3_size(self, mock_prs_class): """测试使用 4:3 尺寸初始化""" mock_prs = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator(size="4:3") assert gen.prs == mock_prs assert hasattr(mock_prs, "slide_width") assert hasattr(mock_prs, "slide_height") @patch("renderers.pptx_renderer.PptxPresentation") def test_init_with_default_size(self, mock_prs_class): """测试默认尺寸""" mock_prs = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator() assert gen.prs == mock_prs @patch("renderers.pptx_renderer.PptxPresentation") def test_init_with_invalid_size_raises_error(self, mock_prs_class): """测试无效尺寸引发错误""" from loaders.yaml_loader import YAMLError with pytest.raises(YAMLError, match="不支持的尺寸比例"): PptxGenerator(size="21:9") class TestAddSlide: """add_slide 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_add_slide_creates_slide(self, mock_prs_class): """测试添加幻灯片""" mock_prs = Mock() mock_layout = Mock() mock_prs.slide_layouts = [None] * 6 + [mock_layout] + [None] mock_prs.slides.add_slide.return_value = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator() slide_data = {"background": None, "elements": []} gen.add_slide(slide_data) # 验证添加了幻灯片 mock_prs.slides.add_slide.assert_called_once_with(mock_layout) @patch("renderers.pptx_renderer.PptxPresentation") def test_add_slide_with_background(self, mock_prs_class): """测试添加带背景的幻灯片""" mock_prs = Mock() mock_slide = Mock() mock_slide.background = Mock() mock_slide.background.fill = Mock() mock_prs.slide_layouts = [None] * 7 + [Mock()] mock_prs.slides.add_slide.return_value = mock_slide mock_prs_class.return_value = mock_prs gen = PptxGenerator() slide_data = {"background": {"color": "#ffffff"}, "elements": []} gen.add_slide(slide_data) # 验证背景被设置 assert ( mock_slide.background.fill.solid.called or mock_slide.background.fill.fore_color_rgb is not None ) @patch("renderers.pptx_renderer.PptxPresentation") def test_add_slide_without_background(self, mock_prs_class): """测试添加无背景的幻灯片""" mock_prs = Mock() mock_slide = Mock() mock_prs.slide_layouts = [None] * 7 + [Mock()] mock_prs.slides.add_slide.return_value = mock_slide mock_prs_class.return_value = mock_prs gen = PptxGenerator() slide_data = {"background": None, "elements": []} gen.add_slide(slide_data) # 不应该崩溃 assert True class TestRenderText: """_render_text 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_render_text_element(self, mock_prs_class): """测试渲染文本元素""" mock_slide = self._setup_mock_slide() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = TextElement( content="Test Content", box=[1, 2, 3, 1], font={"size": 18, "bold": True, "color": "#333333", "align": "center"}, ) gen._render_text(mock_slide, elem) # 验证添加了文本框 mock_slide.shapes.add_textbox.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_text_with_word_wrap(self, mock_prs_class): """测试文本自动换行""" mock_slide = self._setup_mock_slide() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = TextElement(content="Long text", box=[0, 0, 1, 1], font={}) gen._render_text(mock_slide, elem) # 验证文本框被创建 mock_slide.shapes.add_textbox.assert_called_once() def _setup_mock_slide(self): """辅助函数:创建 mock slide""" mock_slide = Mock() mock_text_frame = Mock() mock_text_frame.word_wrap = True mock_paragraph = Mock() mock_paragraph.font = Mock() mock_text_frame.paragraphs = [mock_paragraph] mock_textbox = Mock() mock_textbox.text_frame = mock_text_frame mock_slide.shapes.add_textbox.return_value = mock_textbox return mock_slide class TestRenderImage: """_render_image 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_render_image_element(self, mock_prs_class, temp_dir, sample_image): """测试渲染图片元素""" mock_slide = Mock() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ImageElement(box=[1, 1, 4, 3], src=sample_image.name) gen._render_image(mock_slide, elem, temp_dir) # 验证添加了图片 mock_slide.shapes.add_picture.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_image_nonexistent_file(self, mock_prs_class): """测试不存在的图片文件""" mock_slide = Mock() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ImageElement(box=[1, 1, 4, 3], src="nonexistent.png") with pytest.raises(YAMLError, match="图片文件未找到"): gen._render_image(mock_slide, elem, None) @patch("renderers.pptx_renderer.PptxPresentation") def test_render_image_with_relative_path( self, mock_prs_class, temp_dir, sample_image ): """测试相对路径图片""" mock_slide = Mock() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ImageElement(box=[1, 1, 4, 3], src=sample_image.name) gen._render_image(mock_slide, elem, temp_dir) mock_slide.shapes.add_picture.assert_called_once() class TestRenderShape: """_render_shape 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_render_rectangle(self, mock_prs_class): """测试渲染矩形""" mock_slide = self._setup_mock_slide_for_shape() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ShapeElement(box=[1, 1, 2, 1], shape="rectangle", fill="#4a90e2") gen._render_shape(mock_slide, elem) mock_slide.shapes.add_shape.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_ellipse(self, mock_prs_class): """测试渲染椭圆""" mock_slide = self._setup_mock_slide_for_shape() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ShapeElement(box=[1, 1, 2, 2], shape="ellipse", fill="#e24a4a") gen._render_shape(mock_slide, elem) mock_slide.shapes.add_shape.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_shape_with_line(self, mock_prs_class): """测试带边框的形状""" mock_slide = self._setup_mock_slide_for_shape() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = ShapeElement( box=[1, 1, 2, 1], shape="rectangle", fill="#4a90e2", line={"color": "#000000", "width": 2}, ) gen._render_shape(mock_slide, elem) mock_slide.shapes.add_shape.assert_called_once() def _setup_mock_slide_for_shape(self): """辅助函数:创建用于形状渲染的 mock slide""" mock_slide = Mock() mock_shape = Mock() mock_shape.fill = Mock() mock_shape.fill.solid = Mock() mock_shape.line = Mock() mock_slide.shapes.add_shape.return_value = mock_shape return mock_slide class TestRenderTable: """_render_table 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_render_table(self, mock_prs_class): """测试渲染表格""" mock_slide = self._setup_mock_slide_for_table() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = TableElement( position=[1, 1], col_widths=[2, 2, 2], data=[["A", "B", "C"], ["1", "2", "3"]], style={"font_size": 14}, ) gen._render_table(mock_slide, elem) # 验证添加了表格 mock_slide.shapes.add_table.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_table_with_header_style(self, mock_prs_class): """测试带表头样式的表格""" mock_slide = self._setup_mock_slide_for_table() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = TableElement( position=[1, 1], col_widths=[2, 2], data=[["H1", "H2"], ["D1", "D2"]], style={"font_size": 14, "header_bg": "#4a90e2", "header_color": "#ffffff"}, ) gen._render_table(mock_slide, elem) mock_slide.shapes.add_table.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_render_table_col_widths_mismatch(self, mock_prs_class): """测试列宽不匹配""" mock_slide = self._setup_mock_slide_for_table() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() elem = TableElement( position=[1, 1], col_widths=[2, 3, 4], # 3 列 data=[["A", "B"]], # 2 列数据 style={}, ) with pytest.raises(YAMLError, match="列宽数量"): gen._render_table(mock_slide, elem) def _setup_mock_slide_for_table(self): """辅助函数:创建用于表格渲染的 mock slide""" mock_slide = Mock() # 设置 columns 属性,支持索引访问 class MockColumns: def __init__(self, count): self._cols = [Mock() for _ in range(count)] def __getitem__(self, i): return self._cols[i] mock_table = Mock() mock_table.columns = MockColumns(3) # 设置 add_table 返回值(包含 .table 属性) mock_add_table_result = Mock() mock_add_table_result.table = mock_table mock_slide.shapes.add_table.return_value = mock_add_table_result # 设置 rows mock_table.rows = [Mock()] for row in mock_table.rows: row.cells = [Mock()] for cell in row.cells: cell.text_frame = Mock() cell.text_frame.paragraphs = [Mock()] cell.text_frame.paragraphs[0].font = Mock() return mock_slide class TestSave: """save 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_save_creates_directory(self, mock_prs_class, tmp_path): """测试保存时创建目录""" mock_prs = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator() output_dir = tmp_path / "subdir" / "nested" output_path = output_dir / "output.pptx" gen.save(output_path) # 验证目录被创建 assert output_dir.exists() mock_prs.save.assert_called_once() @patch("renderers.pptx_renderer.PptxPresentation") def test_save_existing_file(self, mock_prs_class, tmp_path): """测试保存已存在的文件""" mock_prs = Mock() mock_prs_class.return_value = mock_prs gen = PptxGenerator() output_path = tmp_path / "output.pptx" gen.save(output_path) mock_prs.save.assert_called_once_with(str(output_path)) class TestRenderBackground: """_render_background 方法测试类""" @patch("renderers.pptx_renderer.PptxPresentation") def test_render_solid_background(self, mock_prs_class): """测试纯色背景""" mock_slide = Mock() mock_slide.background = Mock() mock_slide.background.fill = Mock() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() background = {"color": "#ffffff"} gen._render_background(mock_slide, background) # 验证背景被设置 assert mock_slide.background.fill.solid.called @patch("renderers.pptx_renderer.PptxPresentation") def test_render_no_background(self, mock_prs_class): """测试无背景""" mock_slide = Mock() mock_prs_class.return_value = Mock() mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()] mock_prs_class.return_value.slides.add_slide.return_value = mock_slide gen = PptxGenerator() # 不应该崩溃 gen._render_background(mock_slide, None)