crucible / engine /tests /test_render_pdf.py
s-ttp's picture
Fundamentals table: compact headers + provenance footnotes; print-safe tables
4b4863a verified
Raw History Blame Contribute Delete
7.9 kB
"""The printed Fundamentals and forecast table (the app's "Print / PDF"): the report HTML inside the page's own
stylesheet, every <details> opened, print media, Letter with 0.4in margins, printed by Chrome and read back with
pdfplumber. A long provenance header once pushed both value columns off the printed page (SPGI b3779fcd20362bdf).
Skipped where Playwright, Chrome or pdfplumber is unavailable (the Space does not run the test suite)."""
import os
import pathlib
import sys
import tempfile
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
pytest.importorskip("playwright")
pdfplumber = pytest.importorskip("pdfplumber")
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if not os.path.exists(CHROME):
pytest.skip("Chrome is not installed", allow_module_level=True)
import catalog as C # noqa: E402
import reconcile as RC # noqa: E402
import report_model as RM # noqa: E402
from test_spgi_lineage import good, patched # noqa: E402,F401 (the SPGI-shaped fixture)
CSS = (pathlib.Path(__file__).resolve().parents[2] / "static" / "app.css").read_text()
MARGIN_PT = 0.4 * 72
ROWS = (("Revenue", "REVENUE_TTM", "FORECAST_REVENUE"), ("Operating margin", "OPERATING_MARGIN_TTM", "FORECAST_OPERATING_MARGIN"),
("Net margin", "NET_MARGIN_TTM", "FORECAST_NET_MARGIN"), ("EPS (adjusted basis)", "EPS_ADJUSTED_TTM", "FORECAST_EPS"))
def print_pdf(report_html, out, extra_css=""):
from playwright.sync_api import sync_playwright
page = (f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}{extra_css}</style></head><body class='has-analysis'>"
f"<main><section id='analysis' class='analysis'><div id='report'>{report_html}</div></section></main></body></html>")
with sync_playwright() as p:
b = p.chromium.launch(executable_path=CHROME)
pg = b.new_page()
pg.set_content(page, wait_until="load")
pg.evaluate("document.querySelectorAll('#report details').forEach(d => d.open = true)") # the app's beforeprint
pg.emulate_media(media="print")
pg.pdf(path=out, format="Letter", print_background=True,
margin={k: "0.4in" for k in ("top", "bottom", "left", "right")})
b.close()
def check_fundamentals(pdf_path, heads, values, footnotes):
"""Problems with the printed table (empty = the 12 checks pass).
heads: the two column headings; values: {row label: (historical, forecast)}; footnotes: texts that must be printed."""
bad = []
with pdfplumber.open(pdf_path) as pdf:
page = next((p for p in pdf.pages if "Fundamentals and forecast" in (p.extract_text() or "")), None)
if page is None:
return ["no page carries the Fundamentals and forecast table"]
text = " ".join((page.extract_text() or "").split())
words = page.extract_words(keep_blank_chars=False, use_text_flow=True)
top0 = next(w["top"] for w in words if w["text"] == "Fundamentals")
for h in heads: # 9: both headings readable, not clipped
if " ".join(h.upper().split()) not in text.upper():
bad.append(f"column heading '{h}' is not fully printed")
table_words = []
for label, (hist, fcst) in values.items(): # 1–8: every row shows both values
first = label.split()[0]
lw = next((w for w in words if w["text"] == first and w["top"] > top0), None)
if lw is None:
bad.append(f"row '{label}' is not printed")
continue
line = [w for w in words if abs(w["top"] - lw["top"]) < 4 and w["x0"] >= lw["x0"]]
table_words += line
got = [w["text"] for w in line]
for want, col in ((hist, "historical"), (fcst, "forecast")):
if want not in got:
bad.append(f"{label}: {col} value '{want}' is not visible (row prints {got})")
if hist in got and fcst in got and got.index(hist) > got.index(fcst):
bad.append(f"{label}: values out of column order")
for w in table_words: # 12: inside the printable area
if w["x0"] < MARGIN_PT - 2 or w["x1"] > page.width - MARGIN_PT + 2:
bad.append(f"'{w['text']}' is outside the page margins ({w['x0']:.0f}–{w['x1']:.0f} of {page.width:.0f})")
for i, a in enumerate(table_words): # 10: no cell text overlaps another
for b in table_words[i + 1:]:
if a["x0"] < b["x1"] - 0.5 and b["x0"] < a["x1"] - 0.5 and a["top"] < b["bottom"] - 0.5 and b["top"] < a["bottom"] - 0.5:
bad.append(f"'{a['text']}' overlaps '{b['text']}'")
for f in footnotes: # 11: provenance footnotes printed
if " ".join(f.split()) not in text:
bad.append(f"footnote not printed: '{f[:60]}…'")
return bad
def _spgi_report(st):
cat = C.build_catalog(st, include_deferred=True)
return RM.build_user_report(st, cat, {"summary": [], "thesis": [], "risks": [], "seats": {}}, [])
def _expected(rep):
i = next(i for i, b in enumerate(rep.blocks) if b.css == "rp-fund")
tbl = rep.blocks[i]
vals = {}
for row in tbl.content["rows"]:
label = "".join(f.text for f in row[0])
vals[label] = ("".join(f.text for f in row[1]), "".join(f.text for f in row[2]))
feet = ["".join(f.text for f in b.content) for b in rep.blocks[i + 1:i + 3]]
return tbl.content["head"][1:], vals, feet
def _spgi_state_changes(st):
reports = [{"fiscalDateEnding": e} for e in ("2026-06-30", "2026-03-31", "2025-12-31", "2025-09-30")]
q = RC.ttm_quality({"status": "RECONCILED", "overrides": [], "unverified_quarters": ["2026-06-30"]}, reports)
return [("historical", "reconciliation", dict(st.historical.get("reconciliation"), ttm_quality=q)),
("forecast", "consensus_anchor", {"eps": {"model_weight": 0.0, "reason": "RESTATEMENT"},
"revenue": {"model_weight": 0.0, "reason": "RESTATEMENT"}})]
def test_printed_fundamentals_table_shows_every_value_and_footnote(good):
st = good.state
with patched(st, *_spgi_state_changes(st)):
rep = _spgi_report(st)
heads, vals, feet = _expected(rep)
assert len(vals) == 4 and all(h and f for h, f in vals.values())
assert "through 2026-03-31" in feet[0] and "set from sell-side consensus" in feet[1]
with tempfile.TemporaryDirectory() as d:
out = os.path.join(d, "report.pdf")
print_pdf(RM.render_html(rep), out)
assert check_fundamentals(out, heads, vals, feet) == []
def test_the_check_catches_the_old_clipped_table(good):
"""Negative control: the old long provenance header with the old print behaviour (no wrapping, a scrolling
container) must fail the check, as the published b3779fcd20362bdf PDF did."""
st = good.state
with patched(st, *_spgi_state_changes(st)):
rep = _spgi_report(st)
heads, vals, feet = _expected(rep)
tbl = next(b for b in rep.blocks if b.css == "rp-fund")
tbl.content["head"][1] = RM.ttm_header(RM.historical_quality(st), True)
old_css = "@media print{.rp-scroll{overflow-x:auto!important}.rp-t{width:auto!important;min-width:100%!important}" \
".rp-t th{white-space:nowrap!important}}"
with tempfile.TemporaryDirectory() as d:
out = os.path.join(d, "old.pdf")
print_pdf(RM.render_html(rep), out, extra_css=old_css)
assert check_fundamentals(out, [tbl.content["head"][1], heads[1]], vals, feet)