"""Slide decks: what the model writes is clamped to a fixed shape, laid out by code in either theme, checked against
the sources, and drawn the same way as pages, a PDF and an editable PowerPoint. The LLM is faked at app.llm.run."""
import copy
import io
import re
import pytest
from pptx import Presentation
from app.infographics.image import chrome
from app.slides import export
from app.slides.build import finish, view
from app.slides.content import LIMITS, clamp_deck, clamp_slide
from app.slides.layout import GEOMETRY, VARIANTS, assign_variants, compose
from app.slides.render import deck_print_html, slide_html
from tests.test_buildout import _notebook_with_pricing, auth, feed, llm # noqa: F401 (fixtures)
LONG = "Words that keep going and going " * 40
FULL = {
"title": {"heading": "Pricing that readers say yes to", "kicker": "From 4 posts", "lead": "What two years taught us."},
"agenda": {"heading": "What we will cover", "points": [{"text": f"Part {i}"} for i in range(9)]},
"section": {"heading": "The numbers behind trials", "kicker": "Part two", "lead": "Short trials converted twice as well."},
"points": {"heading": "Why tiers work", "kicker": "Tiers", "lead": "Readers self-select.",
"points": [{"term": "Anchoring", "text": "A higher tier makes the middle one feel fair."}] * 4},
"two_column": {"heading": "Monthly or annual", "left": {"label": "Monthly", "items": ["Low commitment", "Easy to leave"]},
"right": {"label": "Annual", "items": ["Two months free", "Lower churn"]}},
"stat": {"heading": "Short trials win", "stat": {"value": "2x", "label": "more paid signups"}, "lead": "A decision point."},
"quote": {"heading": "In their words", "quote": {"text": "Thursday would feel empty without it.", "by": "A reader"}},
"closing": {"heading": "Key takeaways", "takeaways": ["One.", "Two.", "Three."], "closing": "Price the habit."},
}
def _stuff(slide: dict, text: str) -> dict:
"""The same slide with every text field set to `text` (the longest anything could be)."""
s = copy.deepcopy(slide)
for k in ("heading", "kicker", "lead", "closing"):
if k in s:
s[k] = text
for p in s.get("points") or []:
p["text"], p["term"] = text, text[:60]
for side in ("left", "right"):
if side in s:
s[side] = {"label": text, "items": [text] * 6}
if "stat" in s:
s["stat"] = {"value": "123456789012345678", "label": text}
if "quote" in s:
s["quote"] = {"text": text, "by": text}
if "takeaways" in s:
s["takeaways"] = [text] * 5
return s
@pytest.mark.parametrize("theme", ["dark-space", "light-space"])
@pytest.mark.parametrize("fmt", ["detailed", "presenter"])
def test_every_variant_draws_inside_its_boxes_at_any_length(theme, fmt):
for layout, variants in VARIANTS.items():
for v in variants:
for text in ("Short text here", LONG, "長い日本語のテキスト" * 30, "Supercalifragilistic" * 12):
raw = {**_stuff(FULL[layout], text), "layout": layout, "variant": v}
deck = clamp_deck({"title": "Deck", "slides": [raw, raw]}, fmt)
for i in range(2):
c = compose(deck, i, theme, fmt, seed=7)
for e in c.elements:
assert 0 <= e.x and e.x + e.w <= 1920 + 1 and 0 <= e.y and e.y + e.h <= 1080 + 1, (layout, v, e.role)
if e.kind == "text":
assert e.text_height() <= e.h + 1, (layout, v, e.role, text[:20])
def test_clamp_never_lets_bad_input_through():
garbage = [None, 5, "text", [], {"layout": "nonsense", "heading": "**Bold** [1]"},
{"layout": "stat", "heading": "No number", "stat": {"value": "", "label": "x"}},
{"layout": "quote", "heading": "No quote", "quote": None, "points": [{"text": "a"}, {"text": "b"}]},
{"layout": "two_column", "heading": "One side", "left": {"items": ["x"]}, "right": None},
{"layout": "points", "heading": "Nul\x00 byte— dash", "points": "not a list"},
{"layout": "closing", "heading": "", "takeaways": None, "closing": None},
{"layout": "points", "heading": "x" * 5000, "points": [{"term": None, "text": {"nested": 1}}] * 30}]
deck = clamp_deck({"title": None, "slides": garbage}, "detailed")
assert deck and deck["title"]
keys = set(clamp_slide({"heading": "x"}, "detailed"))
for s in deck["slides"]:
assert set(s) == keys # every slide has every key
for value in (s["heading"], s["lead"], s["kicker"]):
assert "<" not in value and "\x00" not in value and "—" not in value and "**" not in value
assert len(s["heading"]) <= LIMITS["detailed"]["heading"]
layouts = [s["layout"] for s in deck["slides"]]
assert layouts[0] == "points" or layouts[0] == "section" # unknown layout becomes a simpler one
assert "stat" not in layouts and "quote" not in layouts and "two_column" not in layouts
assert clamp_deck({"slides": []}, "detailed") is None and clamp_deck("nope", "presenter") is None
def test_pages_escape_everything_the_model_wrote():
raw = {"layout": "points", "heading": "Fine heading", "lead": "a & b < c",
"points": [{"term": "", "text": "\"quoted\" text"}] * 3}
deck = clamp_deck({"title": "