File size: 7,899 Bytes
4b4863a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """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)
|