"""Text from the data reaches the page as text, in a real browser.
The chart builder's filters and the chart tables built their HTML from strings,
and a currency could hold markup (decision #130). The currency is now refused
on the way in — `test_currency_codes.py` — but a value stored before that still
sits in the database, so what protects an existing install is the page treating
it as text. Only a browser can show that, so these run with the visual tests.
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
import pytest
from freezegun import freeze_time
from sqlalchemy import update
import factories as fac
import fixture_portfolio as ref
from app.models import Instrument
from test_routes import bind_to_only_portfolio, make_login, session_csrf
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from tools.browser import find_chrome # noqa: E402
pytestmark = pytest.mark.visual
CHROME = find_chrome()
if CHROME is None:
pytest.skip("no headless Chrome on this machine", allow_module_level=True)
HTML = {"accept": "text/html"}
STATIC = ROOT / "app" / "static"
# Lower case: it is stored directly, the way a value from before the fix would be.
MARKUP = ""
@pytest.fixture
def poisoned(client, session_factory):
"""Two holdings, one whose currency was stored as markup before the rule."""
make_login(client, session_factory)
with session_factory() as s:
bind_to_only_portfolio(s)
acme = fac.make_instrument(s, "ACME", asset_class="share")
zulu = fac.make_instrument(s, "ZULU", asset_class="share", currency="USD")
fac.add_trade(s, acme, "2026-01-05", "buy", 10, "4.00")
fac.add_trade(s, zulu, "2026-01-05", "buy", 10, "4.00")
s.commit()
# Around the model's validator on purpose: this is what is already there.
s.execute(update(Instrument).where(Instrument.ticker == "ZULU")
.values(currency=MARKUP))
s.commit()
return client
def _in_chrome(page: str, tmp_path: Path) -> str:
"""The page with its scripts inlined, as Chrome leaves it once they ran."""
def inline(match: re.Match) -> str:
path = STATIC / match.group(1)
# idle.js signs the page out after a while; nothing to measure here.
if match.group(1) == "idle.js" or not path.is_file():
return ""
return ""
page = re.sub(r'', inline, page)
target = tmp_path / "page.html"
target.write_text(page, encoding="utf-8")
return subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox",
"--allow-file-access-from-files", "--virtual-time-budget=4000",
target.as_uri()],
capture_output=True, text=True, timeout=90).stdout
def _assert_shown_as_text(dom: str) -> None:
assert "