ppt_agent / tests /tools /test_pptx_tools.py
Berlm's picture
Upload folder using huggingface_hub
c6bc2db verified
Raw History Blame Contribute Delete
11.5 kB
import pytest
from pptx.enum.chart import XL_CHART_TYPE
from pptx.enum.shapes import MSO_CONNECTOR
from pptx.util import Inches
from ppt_agent.server.utils.pptx_functions import PptxEditor
@pytest.fixture
def editor():
return PptxEditor()
def add_blank_slide(editor: PptxEditor) -> int:
return editor.add_slide()
def add_textbox(editor: PptxEditor, slide_index: int, text: str = "sample") -> int:
slide = editor.prs.slides[slide_index]
textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
textbox.text_frame.text = text
return len(slide.shapes) - 1
def test_add_slide_creates_slide(editor):
slide_index = editor.add_slide()
assert slide_index == 0
assert len(editor.prs.slides) == 1
def test_validate_slide_raises_for_invalid_indexes(editor):
add_blank_slide(editor)
with pytest.raises(IndexError, match="Slide index -1 out of range"):
editor._validate_slide(-1)
with pytest.raises(IndexError, match="Slide index 1 out of range"):
editor._validate_slide(1)
def test_get_shape_raises_for_invalid_shape_index(editor):
slide_index = add_blank_slide(editor)
with pytest.raises(IndexError, match="Shape index 0 out of range"):
editor._get_shape(slide_index, 0)
def test_reorder_slide_moves_slide_to_new_position(editor):
for _ in range(3):
add_blank_slide(editor)
original_ids = [slide.slide_id for slide in editor.prs.slides]
editor.reorder_slide(0, 2)
reordered_ids = [slide.slide_id for slide in editor.prs.slides]
assert reordered_ids == [original_ids[1], original_ids[2], original_ids[0]]
def test_reorder_slide_raises_for_invalid_new_index(editor):
add_blank_slide(editor)
with pytest.raises(IndexError, match="new_index out of bounds"):
editor.reorder_slide(0, 1)
def test_insert_text_replaces_existing_text_in_textbox(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index, "Before")
editor.insert_text(slide_index, shape_index, "After")
assert editor.prs.slides[slide_index].shapes[shape_index].text_frame.text == "After"
def test_insert_text_preserves_newline_separated_paragraphs(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index)
editor.insert_text(slide_index, shape_index, "One\nTwo\nThree")
paragraphs = (
editor.prs.slides[slide_index].shapes[shape_index].text_frame.paragraphs
)
assert [paragraph.text for paragraph in paragraphs] == ["One", "Two", "Three"]
def test_insert_text_rejects_non_text_shape(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
slide.shapes.add_connector(
MSO_CONNECTOR.STRAIGHT, Inches(1), Inches(1), Inches(2), Inches(2)
)
with pytest.raises(ValueError, match="Shape has no text frame"):
editor.insert_text(slide_index, 0, "One")
def test_add_chart_uses_default_chart_type_for_unknown_values(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_chart(
slide_index,
"not_a_chart_type",
{"categories": ["A", "B"], "series": [{"name": "Series 1", "values": [1, 2]}]},
1,
1,
4,
3,
)
assert len(slide.shapes) == 1
assert slide.shapes[0].has_chart
assert slide.shapes[0].chart.chart_type == XL_CHART_TYPE.COLUMN_CLUSTERED
def test_add_chart_applies_optional_style(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_chart(
slide_index,
"column_clustered",
{"categories": ["A", "B"], "series": [{"name": "Series 1", "values": [1, 2]}]},
1,
1,
4,
3,
style={
"title": "Styled Chart",
"title_font_name": "Arial",
"title_font_size_pt": 20,
"title_color_hex": "#112233",
"series_colors": ["#445566"],
},
)
chart = slide.shapes[0].chart
assert chart.chart_title.text_frame.text == "Styled Chart"
assert chart.chart_title.text_frame.paragraphs[0].font.name == "Arial"
assert chart.chart_title.text_frame.paragraphs[0].font.size.pt == 20
assert str(chart.chart_title.text_frame.paragraphs[0].font.color.rgb) == "112233"
assert str(chart.series[0].format.fill.fore_color.rgb) == "445566"
def test_style_chart_updates_existing_chart(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_chart(
slide_index,
"column_clustered",
{"categories": ["A", "B"], "series": [{"name": "Series 1", "values": [1, 2]}]},
1,
1,
4,
3,
)
editor.style_chart(
slide_index,
0,
title="Updated Title",
title_font_name="Calibri",
title_font_size_pt=18,
title_bold=True,
title_color_hex="#AA5500",
series_colors=["#003366"],
)
chart = slide.shapes[0].chart
title_font = chart.chart_title.text_frame.paragraphs[0].font
assert chart.chart_title.text_frame.text == "Updated Title"
assert title_font.name == "Calibri"
assert title_font.size.pt == 18
assert title_font.bold is True
assert str(title_font.color.rgb) == "AA5500"
assert str(chart.series[0].format.fill.fore_color.rgb) == "003366"
def test_add_table_populates_available_cells_only(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_table(
slide_index,
2,
2,
[["A1", "A2", "A3"], ["B1"]],
1,
1,
4,
2,
)
table = slide.shapes[0].table
assert table.cell(0, 0).text == "A1"
assert table.cell(0, 1).text == "A2"
assert table.cell(1, 0).text == "B1"
assert table.cell(1, 1).text == ""
def test_add_table_applies_optional_style(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_table(
slide_index,
2,
2,
[["Header 1", "Header 2"], ["Value 1", "Value 2"]],
1,
1,
4,
2,
style={
"header_fill_hex": "#112233",
"body_fill_hex": "#F1F5F9",
"header_font_name": "Arial",
"body_font_name": "Calibri",
"header_font_size_pt": 18,
"body_font_size_pt": 14,
"header_font_color_hex": "#FFFFFF",
"body_font_color_hex": "#334455",
},
)
table = slide.shapes[0].table
assert str(table.cell(0, 0).fill.fore_color.rgb) == "112233"
assert str(table.cell(1, 0).fill.fore_color.rgb) == "F1F5F9"
assert table.cell(0, 0).text_frame.paragraphs[0].font.name == "Arial"
assert table.cell(1, 0).text_frame.paragraphs[0].font.name == "Calibri"
assert table.cell(0, 0).text_frame.paragraphs[0].font.size.pt == 18
assert table.cell(1, 0).text_frame.paragraphs[0].font.size.pt == 14
assert str(table.cell(0, 0).text_frame.paragraphs[0].font.color.rgb) == "FFFFFF"
assert str(table.cell(1, 0).text_frame.paragraphs[0].font.color.rgb) == "334455"
def test_style_table_updates_existing_table(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
editor.add_table(
slide_index,
2,
2,
[["Header 1", "Header 2"], ["Value 1", "Value 2"]],
1,
1,
4,
2,
)
editor.style_table(
slide_index,
0,
header_fill_hex="#0F172A",
body_fill_hex="#E2E8F0",
header_font_name="Georgia",
body_font_name="Verdana",
header_font_size_pt=16,
body_font_size_pt=12,
header_font_color_hex="#FAFAFA",
body_font_color_hex="#1E293B",
)
table = slide.shapes[0].table
assert str(table.cell(0, 1).fill.fore_color.rgb) == "0F172A"
assert str(table.cell(1, 1).fill.fore_color.rgb) == "E2E8F0"
assert table.cell(0, 1).text_frame.paragraphs[0].font.name == "Georgia"
assert table.cell(1, 1).text_frame.paragraphs[0].font.name == "Verdana"
assert str(table.cell(0, 1).text_frame.paragraphs[0].font.color.rgb) == "FAFAFA"
assert str(table.cell(1, 1).text_frame.paragraphs[0].font.color.rgb) == "1E293B"
def test_style_update_applies_font_settings_to_existing_runs(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index, "")
paragraph = (
editor.prs.slides[slide_index].shapes[shape_index].text_frame.paragraphs[0]
)
run = paragraph.add_run()
run.text = "Styled"
editor.style_update(
slide_index,
shape_index,
font_name="Arial",
font_size_pt=18,
bold=True,
italic=True,
color_hex="#112233",
)
assert run.font.name == "Arial"
assert run.font.size.pt == 18
assert run.font.bold is True
assert run.font.italic is True
assert str(run.font.color.rgb) == "112233"
def test_add_textbox_enables_word_wrap_by_default(editor):
slide_index = add_blank_slide(editor)
slide_id = editor.get_slide_id(slide_index)
shape_id = editor.add_textbox_by_id(slide_id, 1, 1, 4, 1)
editor.insert_text_by_id(slide_id, shape_id, "Wrapped by default")
text_frame = editor._get_shape_by_id(slide_id, shape_id).text_frame
assert text_frame.word_wrap is True
def test_style_text_applies_wrap_and_paragraph_spacing(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index, "Line 1\nLine 2")
editor.style_update(
slide_index,
shape_index,
font_name="Arial",
font_size_pt=18,
color_hex="#112233",
word_wrap=False,
space_before_pt=3,
space_after_pt=6,
line_spacing=1.4,
)
paragraphs = (
editor.prs.slides[slide_index].shapes[shape_index].text_frame.paragraphs
)
text_frame = editor.prs.slides[slide_index].shapes[shape_index].text_frame
assert text_frame.word_wrap is False
assert paragraphs[0].space_before.pt == 3
assert paragraphs[0].space_after.pt == 6
assert paragraphs[0].line_spacing == 1.4
def test_style_update_rejects_non_text_shape(editor):
slide_index = add_blank_slide(editor)
slide = editor.prs.slides[slide_index]
slide.shapes.add_connector(
MSO_CONNECTOR.STRAIGHT, Inches(1), Inches(1), Inches(2), Inches(2)
)
with pytest.raises(ValueError, match="Shape has no text frame"):
editor.style_update(slide_index, 0, bold=True)
def test_style_table_rejects_non_table_shape(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index, "Not a table")
with pytest.raises(ValueError, match="Shape has no table"):
editor.style_table(slide_index, shape_index, header_fill_hex="#000000")
def test_style_chart_rejects_non_chart_shape(editor):
slide_index = add_blank_slide(editor)
shape_index = add_textbox(editor, slide_index, "Not a chart")
with pytest.raises(ValueError, match="Shape has no chart"):
editor.style_chart(slide_index, shape_index, title="Nope")
def test_add_image_raises_for_missing_file(editor):
slide_index = add_blank_slide(editor)
with pytest.raises(FileNotFoundError, match="Image path invalid"):
editor.add_image(slide_index, "/tmp/does-not-exist.png", 1, 1)