e07a210c70
CI workflows: install python-pptx, pin CLI versions, stage both PPTX + inlined HTML. test_slides_pipeline.py: inverted theme assertion (now default+inline), deleted source-md tests, added 8 new tests (penetrate absence, image inlining, python-pptx, benefit class, single source, speaker-notes comments, default theme, css retained). New test_pptx_generator.py: slide count, title colors, slide titles, table rendering, image embedding, benefit callout. README rewritten for 3-step single-document + dual-PPTX + image-inlining pipeline. ---ci--- project: acdl phase: 5 milestone: v1.23 status: execute phase_role: execution ---/ci---
145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
"""REQ-274: tests for `scripts/render_pptx.py` — the structured, editable
|
|
python-pptx deck produced alongside the MARP-rendered PPTX.
|
|
|
|
The python-pptx deck is a native OOXML presentation: real text boxes,
|
|
native tables, embedded pictures, and italic benefit callouts. These
|
|
tests are offline (no AWS, no network) and assert the structural
|
|
properties of the committed `*-python.pptx` artifact.
|
|
"""
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
import pytest
|
|
from pptx import Presentation
|
|
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
from pptx.presentation import Presentation as PresentationT
|
|
from pptx.shapes.autoshape import Shape
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PRESENTATIONS = ROOT / "docs" / "presentations"
|
|
MARPT_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
|
|
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
|
|
|
|
# Title slide + 20 main slides + 1 appendix slide.
|
|
EXPECTED_SLIDE_COUNT = 22
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def prs() -> PresentationT:
|
|
"""Load the committed python-pptx deck once for the whole module."""
|
|
assert PYTHON_PPTX.is_file(), f"python-pptx PPTX not found: {PYTHON_PPTX}"
|
|
return Presentation(str(PYTHON_PPTX))
|
|
|
|
|
|
def _slide_titles(prs: PresentationT) -> list[str]:
|
|
"""Return the first non-empty text-frame line per slide (the title)."""
|
|
titles: list[str] = []
|
|
for slide in prs.slides:
|
|
for shape in slide.shapes:
|
|
if not shape.has_text_frame:
|
|
continue
|
|
text = cast(Shape, shape).text_frame.text.strip()
|
|
if not text:
|
|
continue
|
|
# The title is the first non-empty line of the first non-empty
|
|
# text frame we find on the slide.
|
|
first_line = text.split("\n")[0].strip()
|
|
if first_line:
|
|
titles.append(first_line)
|
|
break
|
|
else:
|
|
titles.append("")
|
|
return titles
|
|
|
|
|
|
def test_slide_count(prs: PresentationT):
|
|
"""REQ-269/274: the python-pptx deck has 22 slides
|
|
(title + 20 main + 1 appendix)."""
|
|
assert len(prs.slides) == EXPECTED_SLIDE_COUNT, \
|
|
f"expected {EXPECTED_SLIDE_COUNT} slides, got {len(prs.slides)}"
|
|
|
|
|
|
def test_title_slide_colors(prs: PresentationT):
|
|
"""REQ-269: the title slide (slide 0) has a solid-filled background
|
|
shape carrying the S&P Red (#D6002A) brand color (the title slide is
|
|
a red-bar-on-black layout)."""
|
|
title_slide = prs.slides[0]
|
|
red_found = False
|
|
black_found = False
|
|
for shape in title_slide.shapes:
|
|
fill = getattr(shape, "fill", None)
|
|
if fill is None:
|
|
continue
|
|
try:
|
|
if fill.type != 1: # MSO_FILL.SOLID
|
|
continue
|
|
except Exception:
|
|
continue
|
|
rgb = str(fill.fore_color.rgb).upper()
|
|
if rgb == "D6002A":
|
|
red_found = True
|
|
if rgb == "1B1B1B":
|
|
black_found = True
|
|
assert red_found, \
|
|
"title slide has no solid-fill shape with S&P Red (#D6002A)"
|
|
|
|
|
|
def test_expected_slide_titles(prs: PresentationT):
|
|
"""REQ-269/274: spot-check that key slide titles match the markdown
|
|
deck (The Problem, Nova's Vision, Recap + Ask)."""
|
|
titles = _slide_titles(prs)
|
|
# Build a flat lowercase concatenation for substring checks.
|
|
flat = " | ".join(titles).lower()
|
|
expected = [
|
|
"the problem",
|
|
"nova's vision",
|
|
"recap + ask",
|
|
]
|
|
missing = [t for t in expected if t not in flat]
|
|
assert not missing, \
|
|
f"missing expected slide titles in python-pptx deck: {missing}; " \
|
|
f"found titles: {titles}"
|
|
|
|
|
|
def test_table_rendering(prs: PresentationT):
|
|
"""REQ-269: a slide with a table (the RACI slide) has a native PPTX
|
|
table shape (GraphicFrame with has_table=True)."""
|
|
table_slides = []
|
|
for idx, slide in enumerate(prs.slides):
|
|
for shape in slide.shapes:
|
|
if shape.shape_type == MSO_SHAPE_TYPE.TABLE or getattr(
|
|
shape, "has_table", False
|
|
):
|
|
table_slides.append(idx)
|
|
break
|
|
assert table_slides, \
|
|
"no slide in the python-pptx deck has a native PPTX table shape"
|
|
|
|
|
|
def test_image_embedding(prs: PresentationT):
|
|
"""REQ-269: a slide with an image (the Platform Pipeline slide)
|
|
has a native PPTX picture shape."""
|
|
picture_slides = []
|
|
for idx, slide in enumerate(prs.slides):
|
|
for shape in slide.shapes:
|
|
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
picture_slides.append(idx)
|
|
break
|
|
assert picture_slides, \
|
|
"no slide in the python-pptx deck has a native PPTX picture shape"
|
|
|
|
|
|
def test_benefit_callout_present(prs: PresentationT):
|
|
"""REQ-269/274: at least one slide has an italic text run (the
|
|
benefit callout, rendered as italic body text by render_pptx.py)."""
|
|
italic_runs = 0
|
|
for slide in prs.slides:
|
|
for shape in slide.shapes:
|
|
if not shape.has_text_frame:
|
|
continue
|
|
for paragraph in cast(Shape, shape).text_frame.paragraphs:
|
|
for run in paragraph.runs:
|
|
if run.font.italic and run.text.strip():
|
|
italic_runs += 1
|
|
assert italic_runs > 0, \
|
|
"no italic text runs found in the python-pptx deck (benefit callout)" |