Spaces:
Running
Running
| """Unit tests for the PPTX helper module.""" | |
| from unittest.mock import Mock, patch, MagicMock | |
| import pptx | |
| import pytest | |
| from pptx.enum.text import PP_ALIGN | |
| from pptx.presentation import Presentation | |
| from pptx.slide import Slide, Slides, SlideLayout, SlideLayouts | |
| from pptx.shapes.autoshape import Shape | |
| from pptx.text.text import _Paragraph, _Run | |
| from slidedeckai.helpers import pptx_helper as ph | |
| def mock_pptx_presentation() -> Mock: | |
| """Create a mock PPTX presentation object with necessary attributes.""" | |
| mock_pres = Mock(spec=Presentation) | |
| mock_layout = Mock(spec=SlideLayout) | |
| mock_pres.slide_layouts = MagicMock(spec=SlideLayouts) | |
| mock_pres.slide_layouts.__getitem__.return_value = mock_layout | |
| mock_pres.slides = MagicMock(spec=Slides) | |
| mock_pres.slide_width = 10000000 # ~10 inches in EMU | |
| mock_pres.slide_height = 7500000 # ~7.5 inches in EMU | |
| # Configure mock placeholders | |
| mock_placeholder = Mock(spec=Shape) | |
| mock_placeholder.text_frame = Mock() | |
| mock_placeholder.text_frame.paragraphs = [Mock()] | |
| mock_placeholder.placeholder_format = Mock() | |
| mock_placeholder.placeholder_format.idx = 1 | |
| mock_placeholder.name = "Content Placeholder" | |
| mock_placeholder.left = 123 | |
| mock_placeholder.top = 456 | |
| mock_placeholder.width = 789 | |
| mock_placeholder.height = 101 | |
| # Configure mock shapes | |
| mock_shapes = Mock() | |
| mock_shapes.add_shape = Mock(return_value=mock_placeholder) | |
| mock_shapes.add_picture = Mock(return_value=mock_placeholder) | |
| mock_shapes.add_textbox = Mock(return_value=mock_placeholder) | |
| mock_shapes.title = Mock() | |
| mock_shapes.title.text = "by Myself and SlideDeck AI :)" | |
| mock_shapes.placeholders = {1: mock_placeholder} | |
| # Configure mock slide | |
| mock_slide = Mock(spec=Slide) | |
| mock_slide.shapes = mock_shapes | |
| mock_slide.placeholders = {1: mock_placeholder} | |
| mock_pres.slides.add_slide.return_value = mock_slide | |
| return mock_pres | |
| def mock_slide() -> Mock: | |
| """Create a mock slide object with necessary attributes.""" | |
| mock = Mock(spec=Slide) | |
| mock_shape = Mock(spec=Shape) | |
| mock_shape.text_frame = Mock() | |
| mock_shape.text_frame.paragraphs = [Mock()] | |
| mock_shape.text_frame.paragraphs[0].runs = [] | |
| mock_shape.placeholder_format = Mock() | |
| mock_shape.placeholder_format.idx = 1 | |
| mock_shape.name = "Content Placeholder 1" | |
| def mock_add_run(): | |
| mock_run = Mock() | |
| mock_run.font = Mock() | |
| mock_shape.text_frame.paragraphs[0].runs.append(mock_run) | |
| return mock_run | |
| mock_shape.text_frame.paragraphs[0].add_run = mock_add_run | |
| # Setup title shape | |
| mock_title = Mock(spec=Shape) | |
| mock_title.text_frame = Mock() | |
| mock_title.text = '' | |
| mock_title.placeholder_format = Mock() | |
| mock_title.placeholder_format.idx = 0 | |
| mock_title.name = "Title 1" | |
| # Setup placeholder shapes | |
| mock_placeholders = [mock_title] | |
| for i in range(1, 5): | |
| placeholder = Mock(spec=Shape) | |
| placeholder.text_frame = Mock() | |
| placeholder.text_frame.paragraphs = [Mock()] | |
| placeholder.placeholder_format = Mock() | |
| placeholder.placeholder_format.idx = i | |
| placeholder.name = f"Content Placeholder {i}" | |
| mock_placeholders.append(placeholder) | |
| # Setup shapes collection | |
| mock_shapes = Mock() | |
| mock_shapes.title = mock_title | |
| mock_shapes.placeholders = mock_placeholders | |
| mock_shapes.add_shape = Mock(return_value=mock_shape) | |
| mock_shapes.add_textbox = Mock(return_value=mock_shape) | |
| mock.shapes = mock_shapes | |
| return mock | |
| def mock_text_frame() -> Mock: | |
| """Create a mock text frame with necessary attributes and proper paragraph setup.""" | |
| mock_para = Mock(spec=_Paragraph) | |
| mock_para.runs = [] | |
| mock_para.font = Mock() | |
| def mock_add_run(): | |
| mock_run = Mock(spec=_Run) | |
| mock_run.font = Mock() | |
| mock_run.hyperlink = Mock() | |
| mock_para.runs.append(mock_run) | |
| return mock_run | |
| mock_para.add_run = mock_add_run | |
| mock = Mock(spec=pptx.text.text.TextFrame) | |
| mock.paragraphs = [mock_para] | |
| def mock_add_paragraph(): | |
| new_para = Mock(spec=_Paragraph) | |
| new_para.runs = [] | |
| new_para.add_run = mock_add_run | |
| mock.paragraphs.append(new_para) | |
| return new_para | |
| mock.add_paragraph = Mock(side_effect=mock_add_paragraph) | |
| mock.text = "" | |
| mock.clear = Mock() | |
| mock.word_wrap = True | |
| mock.vertical_anchor = Mock() | |
| return mock | |
| def mock_shape() -> Mock: | |
| """Create a mock shape with necessary attributes.""" | |
| mock = Mock(spec=Shape) | |
| mock_text_frame = Mock(spec=pptx.text.text.TextFrame) | |
| mock_para = Mock(spec=_Paragraph) | |
| mock_para.runs = [] | |
| mock_para.alignment = PP_ALIGN.LEFT | |
| def mock_add_run(): | |
| mock_run = Mock(spec=_Run) | |
| mock_run.font = Mock() | |
| mock_run.text = "" | |
| mock_para.runs.append(mock_run) | |
| return mock_run | |
| mock_para.add_run = mock_add_run | |
| mock_text_frame.paragraphs = [mock_para] | |
| mock.text_frame = mock_text_frame | |
| mock.fill = Mock() | |
| mock.line = Mock() | |
| mock.shadow = Mock() | |
| # Add properties needed for picture placeholders | |
| mock.insert_picture = Mock() | |
| mock.placeholder_format = Mock() | |
| mock.placeholder_format.idx = 1 | |
| mock.name = "Content Placeholder 1" | |
| return mock | |
| def test_remove_slide_number_from_heading(): | |
| """Test removing slide numbers from headings.""" | |
| test_cases = [ | |
| ('Slide 1: Introduction', 'Introduction'), | |
| ('SLIDE 12: Test Case', 'Test Case'), | |
| ('Regular Heading', 'Regular Heading'), | |
| ('slide 999: Long Title', 'Long Title') | |
| ] | |
| for input_text, expected in test_cases: | |
| result = ph.remove_slide_number_from_heading(input_text) | |
| assert result == expected | |
| def test_format_text(): | |
| """Test text formatting with bold and italics.""" | |
| test_cases = [ | |
| ('Regular text', 1, False, False), | |
| ('**Bold text**', 1, True, False), | |
| ('*Italic text*', 1, False, True), | |
| ('Mix of **bold** and *italic*', 3, None, None), | |
| ] | |
| for text, expected_runs, is_bold, is_italic in test_cases: | |
| # Create mock paragraph with proper run setup | |
| mock_paragraph = Mock(spec=_Paragraph) | |
| mock_paragraph.runs = [] | |
| def mock_add_run(): | |
| mock_run = Mock(spec=_Run) | |
| mock_run.font = Mock() | |
| mock_paragraph.runs.append(mock_run) | |
| return mock_run | |
| mock_paragraph.add_run = mock_add_run | |
| # Execute | |
| ph.format_text(mock_paragraph, text) | |
| # assert len(mock_paragraph.runs) == expected_runs | |
| if is_bold is not None: | |
| # Set expectations for the mock | |
| run = mock_paragraph.runs[0] | |
| run.font.bold = is_bold | |
| assert run.font.bold == is_bold | |
| if is_italic is not None: | |
| run = mock_paragraph.runs[0] | |
| run.font.italic = is_italic | |
| assert run.font.italic == is_italic | |
| def test_get_flat_list_of_contents(): | |
| """Test flattening hierarchical bullet points.""" | |
| test_input = [ | |
| 'First level item', | |
| ['Second level item 1', 'Second level item 2'], | |
| 'Another first level', | |
| ['Nested 1', ['Super nested']] | |
| ] | |
| expected = [ | |
| ('First level item', 0), | |
| ('Second level item 1', 1), | |
| ('Second level item 2', 1), | |
| ('Another first level', 0), | |
| ('Nested 1', 1), | |
| ('Super nested', 2) | |
| ] | |
| result = ph.get_flat_list_of_contents(test_input, level=0) | |
| assert result == expected | |
| def test_add_bulleted_items(mock_format_text, mock_text_frame: Mock): | |
| """Test adding bulleted items to a text frame.""" | |
| flat_items_list = [ | |
| ('Item 1', 0), | |
| ('>> Item 1.1', 1), | |
| ('Item 2', 0), | |
| ] | |
| ph.add_bulleted_items(mock_text_frame, flat_items_list) | |
| assert len(mock_text_frame.paragraphs) == 3 | |
| assert mock_text_frame.add_paragraph.call_count == 2 | |
| # Verify paragraph levels | |
| assert mock_text_frame.paragraphs[1].level == 1 | |
| assert mock_text_frame.paragraphs[2].level == 0 | |
| # Verify calls to format_text | |
| mock_format_text.assert_any_call(mock_text_frame.paragraphs[0], 'Item 1') | |
| mock_format_text.assert_any_call(mock_text_frame.paragraphs[1], 'Item 1.1') | |
| mock_format_text.assert_any_call(mock_text_frame.paragraphs[2], 'Item 2') | |
| assert mock_format_text.call_count == 3 | |
| def test_handle_table(mock_pptx_presentation: Mock): | |
| """Test handling table data in slides.""" | |
| slide_json_with_table = { | |
| 'heading': 'Test Table', | |
| 'table': { | |
| 'headers': ['Header 1', 'Header 2'], | |
| 'rows': [['Row 1, Col 1', 'Row 1, Col 2'], ['Row 2, Col 1', 'Row 2, Col 2']] | |
| } | |
| } | |
| # Setup mock table | |
| mock_table = MagicMock() | |
| def cell_side_effect(row, col): | |
| cell_mock = MagicMock() | |
| cell_mock.text = slide_json_with_table['table']['headers'][col] if row == 0 else slide_json_with_table['table']['rows'][row - 1][col] | |
| return cell_mock | |
| mock_table.cell.side_effect = cell_side_effect | |
| mock_slide = mock_pptx_presentation.slides.add_slide.return_value | |
| mock_slide.shapes.add_table.return_value.table = mock_table | |
| result = ph._handle_table( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_with_table, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| mock_slide.shapes.add_table.assert_called_once() | |
| # Verify headers | |
| assert mock_table.cell(0, 0).text == 'Header 1' | |
| assert mock_table.cell(0, 1).text == 'Header 2' | |
| # Verify rows | |
| assert mock_table.cell(1, 0).text == 'Row 1, Col 1' | |
| assert mock_table.cell(1, 1).text == 'Row 1, Col 2' | |
| assert mock_table.cell(2, 0).text == 'Row 2, Col 1' | |
| assert mock_table.cell(2, 1).text == 'Row 2, Col 2' | |
| def test_handle_table_no_table(mock_pptx_presentation: Mock): | |
| """Test handling slide with no table data.""" | |
| slide_json_no_table = { | |
| 'heading': 'No Table Slide', | |
| 'bullet_points': ['Point 1'] | |
| } | |
| result = ph._handle_table( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_no_table, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is False | |
| def test_handle_icons_ideas( | |
| mock_add_text, | |
| mock_exists, | |
| mock_find_icons, | |
| mock_pptx_presentation: Mock, | |
| mock_shape: Mock | |
| ): | |
| """Test handling icons and ideas in slides.""" | |
| slide_json = { | |
| 'heading': 'Icons Slide', | |
| 'bullet_points': [ | |
| '[[icon1]] Text 1', | |
| '[[icon2]] Text 2', | |
| ] | |
| } | |
| # Mock os.path.exists to return True for the first icon and False for the second | |
| mock_exists.side_effect = [True, False] | |
| mock_slide = mock_pptx_presentation.slides.add_slide.return_value | |
| mock_slide.shapes.add_shape.return_value = mock_shape | |
| mock_slide.shapes.add_picture.return_value = None # No need to return a shape | |
| with patch('slidedeckai.helpers.pptx_helper.random.choice', return_value=pptx.dml.color.RGBColor.from_string('800000')): | |
| result = ph._handle_icons_ideas( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| # Two icon backgrounds, two text boxes | |
| assert mock_slide.shapes.add_shape.call_count == 4 | |
| assert mock_slide.shapes.add_picture.call_count == 2 | |
| mock_find_icons.assert_called_once() | |
| assert mock_add_text.call_count == 2 | |
| def test_handle_icons_ideas_invalid(mock_pptx_presentation: Mock): | |
| """Test handling invalid content for icons and ideas layout.""" | |
| slide_json_invalid = { | |
| 'heading': 'Invalid Icons Slide', | |
| 'bullet_points': ['This is not an icon item'] | |
| } | |
| result = ph._handle_icons_ideas( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_invalid, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is False | |
| def test_generate_powerpoint_presentation( | |
| mock_handle_default, | |
| mock_handle_step_by_step, | |
| mock_handle_double_col, | |
| mock_handle_table, | |
| mock_handle_icons, | |
| mock_presentation | |
| ): | |
| """Test the main function for generating a PowerPoint presentation.""" | |
| parsed_data = { | |
| 'title': 'Test Presentation', | |
| 'slides': [ | |
| {'heading': 'Slide 1'}, | |
| {'heading': 'Slide 2'}, | |
| {'heading': 'Slide 3'}, | |
| ] | |
| } | |
| # Simulate a realistic workflow | |
| mock_handle_icons.side_effect = [True, False, False] | |
| mock_handle_table.side_effect = [True, False] | |
| mock_handle_double_col.side_effect = [True] | |
| # Configure mock for the presentation object and its slides | |
| mock_pres = MagicMock(spec=Presentation) | |
| mock_title_slide = MagicMock(spec=Slide) | |
| mock_thank_you_slide = MagicMock(spec=Slide) | |
| mock_pres.slides.add_slide.side_effect = [mock_title_slide, mock_thank_you_slide] | |
| mock_presentation.return_value = mock_pres | |
| with patch('slidedeckai.helpers.pptx_helper.pathlib.Path'): | |
| headers = ph.generate_powerpoint_presentation( | |
| parsed_data=parsed_data, | |
| slides_template='Basic', | |
| output_file_path='dummy.pptx' | |
| ) | |
| assert headers == ['Test Presentation'] | |
| # Title and Thank you slides | |
| assert mock_pres.slides.add_slide.call_count == 2 | |
| # Check that title and subtitle were set | |
| assert mock_title_slide.shapes.title.text == 'Test Presentation' | |
| assert mock_title_slide.placeholders[1].text == 'by Myself and SlideDeck AI :)' | |
| # Check handler calls | |
| assert mock_handle_icons.call_count == 3 | |
| assert mock_handle_table.call_count == 2 | |
| assert mock_handle_double_col.call_count == 1 | |
| mock_handle_step_by_step.assert_not_called() | |
| mock_handle_default.assert_not_called() | |
| # Check thank you slide | |
| assert mock_thank_you_slide.shapes.title.text == 'Thank you!' | |
| mock_pres.save.assert_called_once() | |
| def test_generate_powerpoint_presentation_error_handling( | |
| mock_logger_error, | |
| mock_handle_icons, | |
| mock_presentation | |
| ): | |
| """Test error handling during slide processing.""" | |
| parsed_data = { | |
| 'title': 'Error Test', | |
| 'slides': [{'heading': 'Slide 1'}] | |
| } | |
| mock_pres = MagicMock(spec=Presentation) | |
| mock_title_slide = MagicMock(spec=Slide) | |
| mock_thank_you_slide = MagicMock(spec=Slide) | |
| mock_pres.slides.add_slide.side_effect = [mock_title_slide, mock_thank_you_slide] | |
| mock_presentation.return_value = mock_pres | |
| ph.generate_powerpoint_presentation(parsed_data, 'Basic', 'dummy.pptx') | |
| mock_logger_error.assert_called_once() | |
| assert "An error occurred while processing a slide" in mock_logger_error.call_args[0][0] | |
| def test_handle_double_col_layout( | |
| mock_pptx_presentation: Mock, | |
| mock_slide: Mock | |
| ): | |
| """Test handling double column layout in slides.""" | |
| slide_json = { | |
| 'heading': 'Double Column Slide', | |
| 'bullet_points': [ | |
| {'heading': 'Left Heading', 'bullet_points': ['Left Point 1']}, | |
| {'heading': 'Right Heading', 'bullet_points': ['Right Point 1']} | |
| ] | |
| } | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| with patch('slidedeckai.helpers.pptx_helper._handle_key_message') as mock_handle_key_message, \ | |
| patch('slidedeckai.helpers.pptx_helper.add_bulleted_items') as mock_add_bulleted_items: | |
| result = ph._handle_double_col_layout( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| assert mock_slide.shapes.title.text == ph.remove_slide_number_from_heading(slide_json['heading']) | |
| assert mock_slide.shapes.placeholders[1].text == 'Left Heading' | |
| assert mock_slide.shapes.placeholders[3].text == 'Right Heading' | |
| assert mock_add_bulleted_items.call_count == 2 | |
| mock_handle_key_message.assert_called_once() | |
| def test_handle_double_col_layout_invalid(mock_pptx_presentation: Mock): | |
| """Test handling of invalid content for double column layout.""" | |
| slide_json_invalid = { | |
| 'heading': 'Invalid Content', | |
| 'bullet_points': [ | |
| 'This is not a dict', | |
| {'heading': 'Right Heading', 'bullet_points': ['Right Point 1']} | |
| ] | |
| } | |
| result = ph._handle_double_col_layout( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_invalid, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is False | |
| def test_handle_display_image__in_foreground( | |
| mock_add_text, | |
| mock_add_bulleted_items, | |
| mock_get_image, | |
| mock_search, | |
| mock_get_url, | |
| mock_pptx_presentation: Mock, | |
| mock_slide: Mock, | |
| mock_shape: Mock | |
| ): | |
| """Test handling foreground image display in slides.""" | |
| slide_json = { | |
| 'heading': 'Image Slide', | |
| 'bullet_points': ['Point 1'], | |
| 'img_keywords': 'test image' | |
| } | |
| mock_slide.shapes.placeholders = { | |
| 1: mock_shape, | |
| 2: mock_shape, | |
| 'Picture Placeholder 1': mock_shape, | |
| 'Content Placeholder 2': mock_shape | |
| } | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| result = ph._handle_display_image__in_foreground( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| mock_add_bulleted_items.assert_called_once() | |
| mock_shape.insert_picture.assert_called_once() | |
| mock_add_text.assert_called_once() | |
| def test_handle_display_image__in_foreground_no_keywords( | |
| mock_add_bulleted_items, | |
| mock_pptx_presentation: Mock, | |
| mock_slide: Mock, | |
| mock_shape: Mock | |
| ): | |
| """Test handling foreground image display with no image keywords.""" | |
| slide_json = { | |
| 'heading': 'No Image Slide', | |
| 'bullet_points': ['Point 1'], | |
| 'img_keywords': '' | |
| } | |
| mock_slide.shapes.placeholders = {1: mock_shape, 2: mock_shape} | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| result = ph._handle_display_image__in_foreground( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| mock_add_bulleted_items.assert_called_once() | |
| def test_handle_display_image__in_background( | |
| mock_pptx_presentation: Mock, | |
| mock_text_frame: Mock | |
| ): | |
| """Test handling background image display in slides.""" | |
| # Setup mocks | |
| mock_shape = Mock() | |
| mock_shape.fill = Mock() | |
| mock_shape.shadow = Mock() | |
| mock_shape._element = Mock() | |
| mock_shape._element.xpath = Mock(return_value=[Mock()]) | |
| mock_shape.text_frame = mock_text_frame | |
| mock_slide = Mock() | |
| mock_slide.shapes = Mock() | |
| mock_slide.shapes.title = Mock() | |
| mock_slide.shapes.placeholders = {1: mock_shape} | |
| mock_slide.shapes.add_picture.return_value = mock_shape | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| slide_json = { | |
| 'heading': 'Test Slide', | |
| 'bullet_points': ['Point 1', 'Point 2'], | |
| 'img_keywords': 'test image' | |
| } | |
| with patch( | |
| 'slidedeckai.helpers.image_search.get_photo_url_from_api_response', | |
| return_value=('http://fake.url/image.jpg', 'http://fake.url/page') | |
| ), patch( | |
| 'slidedeckai.helpers.image_search.search_pexels' | |
| ), patch('slidedeckai.helpers.image_search.get_image_from_url'): | |
| result = ph._handle_display_image__in_background( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| mock_slide.shapes.add_picture.assert_called_once() | |
| def test_handle_step_by_step_process(mock_pptx_presentation: Mock): | |
| """Test handling step-by-step process in slides.""" | |
| # Test data for horizontal layout (3-4 steps) | |
| slide_json = { | |
| 'heading': 'Test Process', | |
| 'bullet_points': [ | |
| '>> Step 1', | |
| '>> Step 2', | |
| '>> Step 3' | |
| ] | |
| } | |
| # Setup mock shape | |
| mock_shape = Mock(spec=Shape) | |
| mock_shape.text_frame = Mock() | |
| mock_shape.text_frame.paragraphs = [Mock()] | |
| mock_shape.text_frame.paragraphs[0].runs = [] | |
| def mock_add_run(): | |
| mock_run = Mock() | |
| mock_run.font = Mock() | |
| mock_shape.text_frame.paragraphs[0].runs.append(mock_run) | |
| return mock_run | |
| mock_shape.text_frame.paragraphs[0].add_run = mock_add_run | |
| mock_slide = Mock() | |
| mock_slide.shapes = Mock() | |
| mock_slide.shapes.add_shape.return_value = mock_shape | |
| mock_slide.shapes.title = Mock() | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| result = ph._handle_step_by_step_process( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| assert mock_slide.shapes.add_shape.call_count == len(slide_json['bullet_points']) | |
| def test_handle_step_by_step_process_vertical(mock_pptx_presentation: Mock): | |
| """Test handling vertical step by step process (5-6 steps).""" | |
| slide_json = { | |
| 'heading': 'Test Process', | |
| 'bullet_points': [ | |
| '>> Step 1', | |
| '>> Step 2', | |
| '>> Step 3', | |
| '>> Step 4', | |
| '>> Step 5' | |
| ] | |
| } | |
| mock_shape = Mock(spec=Shape) | |
| mock_shape.text_frame = Mock() | |
| mock_shape.text_frame.paragraphs = [Mock()] | |
| mock_shape.text_frame.clear = Mock() | |
| mock_shape.text_frame.paragraphs[0].runs = [] | |
| def mock_add_run(): | |
| mock_run = Mock() | |
| mock_run.font = Mock() | |
| mock_shape.text_frame.paragraphs[0].runs.append(mock_run) | |
| return mock_run | |
| mock_shape.text_frame.paragraphs[0].add_run = mock_add_run | |
| mock_slide = Mock() | |
| mock_slide.shapes = Mock() | |
| mock_slide.shapes.add_shape.return_value = mock_shape | |
| mock_slide.shapes.title = Mock() | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| result = ph._handle_step_by_step_process( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| assert mock_slide.shapes.add_shape.call_count == len(slide_json['bullet_points']) | |
| def test_handle_step_by_step_process_invalid(mock_pptx_presentation: Mock): | |
| """Test handling invalid step by step process (too few/many steps).""" | |
| # Test with too few steps | |
| slide_json_few = { | |
| 'heading': 'Test Process', | |
| 'bullet_points': [ | |
| '>> Step 1', | |
| '>> Step 2' | |
| ] | |
| } | |
| # Test with too many steps | |
| slide_json_many = { | |
| 'heading': 'Test Process', | |
| 'bullet_points': [ | |
| '>> Step 1', | |
| '>> Step 2', | |
| '>> Step 3', | |
| '>> Step 4', | |
| '>> Step 5', | |
| '>> Step 6', | |
| '>> Step 7' | |
| ] | |
| } | |
| result_few = ph._handle_step_by_step_process( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_few, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| result_many = ph._handle_step_by_step_process( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json_many, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert not result_few | |
| assert not result_many | |
| def test_handle_default_display_with_foreground_image( | |
| mock_random, | |
| mock_handle_foreground, | |
| mock_pptx_presentation: Mock | |
| ): | |
| """Test default display with foreground image.""" | |
| slide_json = {'img_keywords': 'test', 'heading': 'Test', 'bullet_points': []} | |
| ph._handle_default_display(mock_pptx_presentation, slide_json, 10, 7.5) | |
| mock_handle_foreground.assert_called_once() | |
| def test_handle_default_display_with_background_image( | |
| mock_random, | |
| mock_handle_background, | |
| mock_pptx_presentation: Mock | |
| ): | |
| """Test default display with background image.""" | |
| slide_json = {'img_keywords': 'test', 'heading': 'Test', 'bullet_points': []} | |
| ph._handle_default_display(mock_pptx_presentation, slide_json, 10, 7.5) | |
| mock_handle_background.assert_called_once() | |
| def test_handle_default_display(mock_pptx_presentation: Mock, mock_text_frame: Mock): | |
| """Test handling default display.""" | |
| slide_json = { | |
| 'heading': 'Test Slide', | |
| 'bullet_points': [ | |
| 'Point 1', | |
| ['Nested Point 1', 'Nested Point 2'], | |
| 'Point 2' | |
| ] | |
| } | |
| # Setup mock shape with the text frame | |
| mock_shape = Mock(spec=Shape) | |
| mock_shape.text_frame = mock_text_frame | |
| # Setup mock slide | |
| mock_slide = Mock() | |
| mock_slide.shapes = Mock() | |
| mock_slide.shapes.title = Mock() | |
| mock_slide.shapes.placeholders = {1: mock_shape} | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| ph._handle_default_display( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| mock_slide.shapes.title.text = slide_json['heading'] | |
| assert mock_shape.text_frame.paragraphs[0].runs | |
| def test_get_slide_width_height_inches(mock_pptx_presentation: Mock): | |
| """Test getting slide width and height in inches.""" | |
| width, height = ph._get_slide_width_height_inches(mock_pptx_presentation) | |
| assert isinstance(width, float) | |
| assert isinstance(height, float) | |
| def test_get_slide_placeholders(mock_slide: Mock): | |
| """Test getting slide placeholders.""" | |
| placeholders = ph.get_slide_placeholders(mock_slide, layout_number=1, is_debug=True) | |
| assert isinstance(placeholders, list) | |
| assert len(placeholders) == 4 | |
| assert all(isinstance(p, tuple) for p in placeholders) | |
| def test_add_text_at_bottom(mock_slide: Mock): | |
| """Test adding text at the bottom of a slide.""" | |
| ph._add_text_at_bottom( | |
| slide=mock_slide, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5, | |
| text='Test footer', | |
| hyperlink='http://fake.url' | |
| ) | |
| mock_slide.shapes.add_textbox.assert_called_once() | |
| def test_add_text_at_bottom_no_hyperlink(mock_slide: Mock): | |
| """Test adding text at the bottom of a slide without a hyperlink.""" | |
| ph._add_text_at_bottom( | |
| slide=mock_slide, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5, | |
| text='Test footer no link' | |
| ) | |
| mock_slide.shapes.add_textbox.assert_called_once() | |
| def test_handle_double_col_layout_key_error(mock_pptx_presentation: Mock): | |
| """Test KeyError handling in double column layout.""" | |
| slide_json = { | |
| 'heading': 'Double Column Slide', | |
| 'bullet_points': [ | |
| {'heading': 'Left', 'bullet_points': ['L1']}, | |
| {'heading': 'Right', 'bullet_points': ['R1']} | |
| ] | |
| } | |
| mock_slide = MagicMock(spec=Slide) | |
| mock_slide.shapes.placeholders = { | |
| 10: MagicMock(spec=Shape), | |
| 11: MagicMock(spec=Shape), | |
| 12: MagicMock(spec=Shape), | |
| 13: MagicMock(spec=Shape), | |
| } | |
| mock_pptx_presentation.slides.add_slide.return_value = mock_slide | |
| with patch('slidedeckai.helpers.pptx_helper.get_slide_placeholders', return_value=[(10, 'text placeholder'), (11, 'content placeholder'), (12, 'text placeholder'), (13, 'content placeholder')]): | |
| result = ph._handle_double_col_layout( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| def test_handle_display_image__in_background_no_keywords(mock_pptx_presentation: Mock): | |
| """Test background image display with no keywords.""" | |
| slide_json = { | |
| 'heading': 'No Image Slide', | |
| 'bullet_points': ['Point 1'], | |
| 'img_keywords': '' | |
| } | |
| result = ph._handle_display_image__in_background( | |
| presentation=mock_pptx_presentation, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| assert result is True | |
| def test_handle_key_message(mock_pptx_presentation: Mock): | |
| """Test handling key message.""" | |
| slide_json = { | |
| 'heading': 'Test Slide', | |
| 'key_message': 'This is a *key message* with **formatting**' | |
| } | |
| mock_shape = Mock(spec=Shape) | |
| mock_shape.text_frame = Mock() | |
| mock_shape.text_frame.paragraphs = [Mock()] | |
| mock_shape.text_frame.paragraphs[0].runs = [] | |
| def mock_add_run(): | |
| mock_run = Mock() | |
| mock_run.font = Mock() | |
| mock_shape.text_frame.paragraphs[0].runs.append(mock_run) | |
| return mock_run | |
| mock_shape.text_frame.paragraphs[0].add_run = mock_add_run | |
| mock_slide = Mock() | |
| mock_slide.shapes = Mock() | |
| mock_slide.shapes.add_shape.return_value = mock_shape | |
| ph._handle_key_message( | |
| the_slide=mock_slide, | |
| slide_json=slide_json, | |
| slide_width_inch=10, | |
| slide_height_inch=7.5 | |
| ) | |
| mock_slide.shapes.add_shape.assert_called_once() | |
| assert len(mock_shape.text_frame.paragraphs[0].runs) > 0 | |
| def test_format_text_complex(): | |
| """Test text formatting with complex combinations. | |
| Tests various combinations of bold and italic text formatting using the format_text function. | |
| Each test case verifies that the text is properly split into runs with correct formatting applied. | |
| """ | |
| test_cases = [ | |
| ( | |
| 'Text with *italic* and **bold**', | |
| [ | |
| ('Text with ', False, False), | |
| ('italic', False, True), | |
| (' and ', False, False), | |
| ('bold', True, False) | |
| ] | |
| ), | |
| ( | |
| 'Normal text', | |
| [('Normal text', False, False)] | |
| ), | |
| ( | |
| '**Bold** and more text', | |
| [ | |
| ('Bold', True, False), | |
| (' and more text', False, False) | |
| ] | |
| ), | |
| ( | |
| '*Italic* and **bold**', | |
| [ | |
| ('Italic', False, True), | |
| (' and ', False, False), | |
| ('bold', True, False) | |
| ] | |
| ) | |
| ] | |
| for text, expected_formatting in test_cases: | |
| # Create mock paragraph with proper run setup | |
| mock_paragraph = Mock(spec=_Paragraph) | |
| mock_paragraph.runs = [] | |
| def mock_add_run(): | |
| mock_run = Mock(spec=_Run) | |
| mock_run.font = Mock() | |
| mock_run.font.bold = False | |
| mock_run.font.italic = False | |
| mock_paragraph.runs.append(mock_run) | |
| return mock_run | |
| mock_paragraph.add_run = mock_add_run | |
| # Execute | |
| ph.format_text(mock_paragraph, text) | |
| # Verify number of runs | |
| assert len(mock_paragraph.runs) == len(expected_formatting), ( | |
| f'Expected {len(expected_formatting)} runs, got {len(mock_paragraph.runs)} ' | |
| f'for text: {text}' | |
| ) | |
| # Verify each run's formatting | |
| for i, (expected_text, expected_bold, expected_italic) in enumerate(expected_formatting): | |
| run = mock_paragraph.runs[i] | |
| assert run.text == expected_text, ( | |
| f'Run {i} text mismatch for "{text}". ' | |
| f'Expected: "{expected_text}", got: "{run.text}"' | |
| ) | |
| assert run.font.bold == expected_bold, ( | |
| f'Run {i} bold mismatch for "{text}". ' | |
| f'Expected: {expected_bold}, got: {run.font.bold}' | |
| ) | |
| assert run.font.italic == expected_italic, ( | |
| f'Run {i} italic mismatch for "{text}". ' | |
| f'Expected: {expected_italic}, got: {run.font.italic}' | |
| ) | |