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)