Spaces:
Sleeping
Sleeping
File size: 15,110 Bytes
93f9cc0 | 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | # -*- coding: utf-8 -*-
"""The note must render as the published note, not merely in its spirit.
The brief was asked to reproduce a specific published document exactly β same
colours, same placements. "Looks close" is not the requirement, so these tests
compare against the document itself rather than against a description of it.
``tests/fixtures/published_note.html`` holds the two things that can be
compared mechanically: the stylesheet, and the three figures. Everything else
about the page is data-dependent, so those assertions are structural β the ten
sections in their published order, six tiles in a six-column grid, a section
that keeps its slot when its data is missing.
Two divergences from the published file are deliberate, and each is asserted
here so it cannot drift into an accidental third:
* every selector carries a ``.rt-note`` prefix, because Streamlit's own base
styles would otherwise win on specificity;
* every ``<svg>`` carries ``xmlns``, because Streamlit's sanitiser drops an
SVG without it and the figure renders as an empty box.
"""
from __future__ import annotations
import pathlib
import re
import pytest
FIXTURE = pathlib.Path(__file__).parent / "fixtures" / "published_note.html"
@pytest.fixture(scope="module")
def published() -> str:
return FIXTURE.read_text(encoding="utf-8")
# ββ The stylesheet βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _rules(css: str) -> list[tuple[str, str]]:
"""Flatten CSS to ``(selector, sorted declarations)`` pairs, in order."""
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
out = []
for selector, block in re.findall(r"([^{}]+)\{([^{}]*)\}", css):
selector = " ".join(selector.split())
if selector.startswith("@"):
continue
declarations = ";".join(
sorted(d.strip() for d in block.split(";") if d.strip())
)
out.append((selector, declarations))
return out
def _unscope(selector: str) -> str:
"""Strip the ``.rt-note`` prefix so selectors can be compared."""
parts = []
for part in selector.split(","):
part = part.strip()
if part == ".rt-note":
part = "body" # the note is a div here, a document there
elif part.startswith(".rt-note "):
part = part[len(".rt-note "):]
parts.append(part)
return ",".join(parts)
# Rules we add on top of the published stylesheet, each with its reason.
# Anything not listed here is drift, and the test says so. Empty is the goal:
# the stylesheet is currently the published one, rule for rule.
ADDED_RULES: dict[str, str] = {}
def test_stylesheet_declarations_are_the_published_ones(published):
"""Every published rule, in order, with the same declarations and values."""
from dashboard.note_css import NOTE_CSS, TOKENS
original = _rules(published[published.index("<style>") + 7: published.index("</style>")])
# margin:0 belongs to the real <body> there; here the note is a div inside
# a page that already sets it.
original = [
(sel, ";".join(d for d in decl.split(";") if d != "margin:0") if sel == "body" else decl)
for sel, decl in original
]
mine = [(_unscope(sel), decl) for sel, decl in _rules(TOKENS + NOTE_CSS)]
extra = [(sel, decl) for sel, decl in mine if sel in ADDED_RULES]
assert len(extra) == len(ADDED_RULES), (
f"expected the additions {sorted(ADDED_RULES)}, found {[s for s, _ in extra]}"
)
kept = [(sel, decl) for sel, decl in mine if sel not in ADDED_RULES]
assert len(kept) == len(original), (
f"rule count drifted: published {len(original)}, ours {len(kept)}. "
f"A new rule needs an entry in ADDED_RULES with its reason."
)
for (sel_a, decl_a), (sel_b, decl_b) in zip(original, kept):
assert sel_a.replace(" ", "") == sel_b.replace(" ", ""), (
f"selector drifted: published {sel_a!r}, ours {sel_b!r}"
)
assert decl_a == decl_b, f"{sel_a}: published {decl_a!r}, ours {decl_b!r}"
def test_every_selector_is_scoped_to_the_note():
"""An unscoped selector would restyle the whole Streamlit page."""
from dashboard.note_css import NOTE_CSS
for selector, _ in _rules(NOTE_CSS):
for part in selector.split(","):
part = part.strip()
assert part == ".rt-note" or part.startswith(".rt-note "), (
f"{part!r} escapes the note's scope"
)
def test_the_light_tokens_are_the_published_values():
"""The palette the app is pinned to is the note's own light palette."""
from dashboard.note_css import TOKENS, TOKENS_LIGHT
def values(css: str) -> dict[str, str]:
block = css[css.index(":root{") + 6: css.index("}", css.index(":root{"))]
out = {}
for decl in block.split(";"):
if "--" in decl:
name, _, value = decl.partition(":")
out[name.strip()] = value.strip()
return out
published_light = values(TOKENS)
pinned = values(TOKENS_LIGHT)
for name, value in published_light.items():
assert pinned.get(name) == value, f"{name}: pinned {pinned.get(name)!r} != {value!r}"
# ββ The figures ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _normalise(svg: str) -> str:
"""Compare on rendered output, not on the two encodings we must differ in.
Undone here: the ``xmlns`` the sanitiser requires, and ``$`` for the
dollar sign, which st.markdown would otherwise read as LaTeX. Both render
exactly as the published note does; everything else must match literally.
"""
svg = re.sub(r">\s+<", "><", svg.strip())
return svg.replace(' xmlns="http://www.w3.org/2000/svg"', "").replace("$", "$")
def _fixture_figure(published: str, name: str) -> str:
block = re.search(rf'<figure id="{name}">(.*?)</figure>', published, re.S)
assert block, f"fixture has no figure {name!r}"
return block.group(1)
def _footnote(svg: str) -> str:
from html import unescape
match = re.search(r'<text x="44" y="16" class="ct">([^<]*)</text>', svg)
return unescape(match.group(1)) if match else ""
def test_bar_figure_geometry_is_identical(published):
"""Same bars, ticks, labels and coordinates as the published figure.
The series is recovered from the published SVG's own geometry rather than
from its printed labels: the note plots the unrounded value and prints the
rounded one, so reading the labels back would move every bar by a tenth of
a pixel and make the comparison meaningless.
"""
from dashboard.note_charts import bars_vs_guidance
svg = _fixture_figure(published, "revenue_vs_guidance")
scale = (274.0 - 50.4) / 100.0
bars = re.findall(
r'<rect x="[\d.]+" y="([\d.]+)" width="[\d.]+" height="[\d.]+" '
r'fill="var\((--[a-z0-9-]+)\)"',
svg,
)
labels = re.findall(r'y="290" text-anchor="middle" class="ct">([^<]+)</text>', svg)
printed = re.findall(r'class="cv">([\d,]+)</text>', svg)
deltas = re.findall(r'y="306" text-anchor="middle" class="cb">([^<]+)</text>', svg)
tick_y = re.findall(r'y1="([\d.]+)" y2="[\d.]+" stroke="var\(--warn\)"', svg)
guides = [None] + [(274.0 - float(y)) / scale for y in tick_y]
points = [
{
"label": label,
"value": (274.0 - float(bar[0])) / scale,
"value_text": printed[n],
"guide": guides[n],
"delta": deltas[n - 1] if n else "",
"derived": bar[1] == "--ink-3",
}
for n, (bar, label) in enumerate(zip(bars, labels))
]
assert len(points) == 14
mine = bars_vs_guidance(
points,
label="Chiffre dβaffaires trimestriel et point mΓ©dian de la guidance",
footnote=_footnote(svg),
)
assert _normalise(mine) == _normalise(svg)
def test_paired_figure_geometry_is_identical(published):
from dashboard.note_charts import paired_bars
svg = _fixture_figure(published, "net_income_vs_fcf")
scale = (250.0 - 59.7) / 60.0
bars = re.findall(
r'<rect x="[\d.]+" y="([\d.]+)" width="[\d.]+" height="[\d.]+" fill="var\(--[a-z0-9-]+\)"',
svg,
)
printed = re.findall(r'class="cv">([\d,]+)</text>', svg)
labels = re.findall(r'y="266" text-anchor="middle" class="ct">([^<]+)</text>', svg)
ratios = re.findall(r'y="284" text-anchor="middle" class="(cb(?: warn)?)">([^<]+)</text>', svg)
groups = [
{
"label": label,
"left": (250.0 - float(bars[2 * n])) / scale,
"left_text": printed[2 * n],
"right": (250.0 - float(bars[2 * n + 1])) / scale,
"right_text": printed[2 * n + 1],
"ratio": ratios[n][1],
"ratio_warn": "warn" in ratios[n][0],
}
for n, label in enumerate(labels)
]
mine = paired_bars(
groups,
label="RΓ©sultat net contre free cash-flow par trimestre",
footnote=_footnote(svg),
)
assert _normalise(mine) == _normalise(svg)
def test_line_figure_geometry_is_identical(published):
from dashboard.note_charts import line_series
svg = _fixture_figure(published, "days_sales_outstanding")
scale = (226.0 - 28.0) / 40.0
dots = re.findall(r'<circle cx="[\d.]+" cy="([\d.]+)" r="(\d)"', svg)
printed = re.findall(r'class="cv">([\d,]+)</text>', svg)
labels = re.findall(r'y="242" text-anchor="middle" class="ct">([^<]+)</text>', svg)
points = [
{
"label": label,
"value": 30.0 + (226.0 - float(cy)) / scale,
"value_text": text,
"emphasis": radius == "6",
}
for (cy, radius), text, label in zip(dots, printed, labels)
]
mine = line_series(
points,
label="DΓ©lai de recouvrement clients en jours",
footnote=_footnote(svg),
)
assert _normalise(mine) == _normalise(svg)
def test_figures_declare_their_namespace():
"""Without xmlns, Streamlit's sanitiser drops the SVG and the figure empties."""
from dashboard.note_charts import bars_vs_guidance, line_series, paired_bars
bars = bars_vs_guidance([{"label": "Q1", "value": 1.0}], label="x")
pair = paired_bars([{"label": "Q1", "left": 1.0, "right": 0.5}], label="x")
line = line_series([{"label": "Q1", "value": 1.0}], label="x")
for svg in (bars, pair, line):
assert svg.startswith('<svg xmlns="http://www.w3.org/2000/svg"')
def test_figures_never_emit_a_bare_dollar_sign():
"""A pair of raw dollar signs is read as LaTeX by st.markdown."""
from dashboard.note_charts import bars_vs_guidance
svg = bars_vs_guidance(
[{"label": "Q1", "value": 1.0}], label="x", footnote="bn$ per share"
)
assert "$" not in svg
assert "$" in svg
def test_the_stylesheet_contains_no_angle_bracket():
"""A "<" anywhere in the CSS truncates the <style> element that carries it.
This is not hypothetical: a comment in this stylesheet that mentioned the
svg tag by name cut the element off at that point, dropping every rule
after it β the note's grid, its sticky rail, and the app chrome CSS
concatenated behind it. The page still rendered, just with no layout.
"""
from dashboard.note_css import FONT_IMPORT, NOTE_CSS, TOKENS, TOKENS_LIGHT
for name, css in (("FONT_IMPORT", FONT_IMPORT), ("TOKENS", TOKENS),
("TOKENS_LIGHT", TOKENS_LIGHT), ("NOTE_CSS", NOTE_CSS)):
assert "<" not in css, f"{name} contains '<', which truncates the style element"
assert "</" not in css, f"{name} contains '</'"
# ββ The document's structure βββββββββββββββββββββββββββββββββββββββββββββββββ
PUBLISHED_SECTIONS = (
"resume", "print", "cash", "bilan", "segments",
"direction", "capital", "bullbear", "watch", "annexes",
)
@pytest.fixture
def thin_brief() -> dict:
"""A brief with nothing in it β the case that must not lose a section."""
return {
"ticker": "ZZZZ",
"company_name": "Nothing Ingested Inc",
"filing_date": "2026-01-31",
"what_matters_most": "",
"what_changed": [],
"bull_points": [],
"bear_points": [],
"what_to_watch": [],
}
def _sections(html: str) -> list[str]:
return re.findall(r'<section id="([a-z]+)"', html)
def test_sections_are_the_published_ones_in_order(thin_brief):
"""The ten sections, in the published order, from any brief."""
from dashboard.note import build
assert tuple(_sections(build(thin_brief, series=[], market=None, lang="fr"))) == PUBLISHED_SECTIONS
def test_a_section_with_no_data_keeps_its_slot(thin_brief):
"""A section that vanished when empty would move every heading below it."""
from dashboard.note import build
html = build(thin_brief, series=[], market=None, lang="en")
assert len(_sections(html)) == len(PUBLISHED_SECTIONS)
# and it says so rather than standing empty
assert "ZZZZ" in html
def test_the_tile_row_is_always_six_wide(thin_brief):
"""The grid is repeat(6,1fr): five tiles would leave a hole in the rule."""
from dashboard.note import build
for brief, series in ((thin_brief, []),):
html = build(brief, series=series, market=None, lang="fr")
assert html.count('class="tile"') == 6
def test_the_document_never_emits_a_bare_dollar_sign(thin_brief):
"""st.markdown reads a pair of dollar signs as LaTeX and eats the middle."""
from dashboard.note import build
assert "$" not in build(thin_brief, series=[], market=None, lang="en")
def test_the_masthead_and_rail_are_always_present(thin_brief):
from dashboard.note import build
html = build(thin_brief, series=[], market=None, lang="fr")
for marker in ('class="mast"', 'class="head"', 'nav class="toc"',
'class="grid"', "<aside>", 'class="foot"'):
assert marker in html, f"{marker} missing"
def test_currency_follows_the_language(thin_brief):
""""96,2 Md$" in French, "$96.2bn" in English β same amount, same tile."""
from dashboard.note import _money, _per_share
with_lang = lambda fn, lang: fn(96_200_000_000, lang=lang) if fn is _money else fn(2.46, lang=lang)
assert _money(96_200_000_000, lang="fr") == "96,2 Md$"
assert _money(96_200_000_000, lang="en") == "$96.2bn"
assert _per_share(2.46, lang="fr") == "2,46 $"
assert _per_share(2.46, lang="en") == "$2.46"
def test_rounding_is_half_up_like_a_printed_table():
"""13.25 prints as 13.3, the way the filing does β not 13.2."""
from dashboard.note import _num
assert _num(13.25, 1, lang="en") == "13.3"
assert _num(13.25, 1, lang="fr") == "13,3"
|