"""The chart builder, driven in a real browser. The builder keeps a draft chart in the page and shows it as chips on three shelves, a type, a grouping, a date and some filter boxes; the preview and the saved chart are drawn from the draft. Its tests read the page's markup. So a seeded walk here places fields by clicking and by dropping, removes them, switches the kind of chart, the type, the grouping and the date, ticks filters and loads templates, while the previews come back late and out of order. After every step the shelves must hold nothing they cannot (a field twice, a field from another kind of chart, a measure where a dimension goes), a template must load as itself, and at the end the preview must be of the chart on the shelves and saving must save that chart. """ from __future__ import annotations import json import re import subprocess import pytest from test_trade_dialog_in_the_browser import CHROME, HTML, _standalone import factories as fac from test_routes import bind_to_only_portfolio, make_login pytestmark = pytest.mark.visual # Previews answer after a seeded delay that is often longer than the # builder's 180ms debounce, echoing the draft they were asked about; saves # answer at once. The renderer is replaced by a recorder: what is drawn is the # question here, not how Chart.js draws it. STUBS = """ """ WALK = """ """ def _page(client, session_factory) -> str: """Two holdings in two classes and two currencies, so the filter shelf has something to choose between.""" make_login(client, session_factory) with session_factory() as s: bind_to_only_portfolio(s) alpha = fac.make_instrument(s, "ALPHA", asset_class="etf") zulu = fac.make_instrument(s, "ZULU", asset_class="share", exchange="NASDAQ", currency="USD") fac.add_trade(s, alpha, "2026-01-05", "buy", 10, "5.00") fac.add_trade(s, zulu, "2026-01-05", "buy", 10, "5.00", fx_rate="1.50") s.commit() page = client.get("/charts/build", headers=HTML) assert page.status_code == 200 return page.text def _run(client, session_factory, tmp_path, seeds, delay="Math.floor(rand() * 600)"): target = tmp_path / "builder.html" driver = WALK.replace("SEEDS", json.dumps(seeds)).replace("STEPS", "40") target.write_text(_standalone(_page(client, session_factory), driver) .replace("
", "" + STUBS.replace("DELAY", delay), 1), encoding="utf-8") dom = subprocess.run( [CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox", "--allow-file-access-from-files", "--window-size=1400,1000", "--virtual-time-budget=600000", target.as_uri()], capture_output=True, text=True, timeout=240).stdout found = re.search(r'(.*?)', dom, re.S) assert found, "the driver did not finish" return json.loads(found.group(1).replace(""", '"').replace("&", "&")) @pytest.mark.parametrize("seeds", [[1, 2, 3], [4, 5, 6]]) def test_the_draft_on_the_shelves_is_the_chart_previewed_and_saved(client, session_factory, tmp_path, seeds): runs = _run(client, session_factory, tmp_path, seeds) for run in runs: assert run["problems"] == [], (run["seed"], run["problems"][:3], run["log"]) done = {line.split()[0] for run in runs for line in run["log"]} assert {"click", "drop", "remove", "grain", "type", "since", "filter", "template"} <= done # The first preview is slow and the next quick, so the first lands last; and # then the shelves are emptied while a third is out. RACE = """ """ def test_a_preview_that_arrives_late_does_not_replace_a_newer_one(client, session_factory, tmp_path): target = tmp_path / "builder.html" target.write_text(_standalone(_page(client, session_factory), RACE).replace( "", "" + STUBS.replace("DELAY", "window.asked.length % 2 ? 800 : 10"), 1), encoding="utf-8") dom = subprocess.run( [CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox", "--allow-file-access-from-files", "--window-size=1400,1000", "--virtual-time-budget=60000", target.as_uri()], capture_output=True, text=True, timeout=120).stdout out = json.loads(re.search(r'
(.*?)', dom, re.S).group(1) .replace(""", '"').replace("&", "&")) assert out["asked"] == 3, "the walk did not ask what it meant to" assert out["afterTwo"] == [["value", "invested"]] # the slow first never drawn assert out["last"] is None # nothing drawn once undrawable assert out["message"] == "Pick an X axis and a measure." # The two placements the walk never happened on: the date dropped on the split # shelf of a daily series while the X axis is empty, and a holdings chart's # split field dropped onto the X axis. SPLITS = """ """ def test_a_split_the_chart_cannot_draw_is_not_placed(client, session_factory, tmp_path): target = tmp_path / "builder.html" target.write_text(_standalone(_page(client, session_factory), SPLITS).replace( "", "" + STUBS.replace("DELAY", "0"), 1), encoding="utf-8") dom = subprocess.run( [CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox", "--allow-file-access-from-files", "--window-size=1400,1000", "--virtual-time-budget=20000", target.as_uri()], capture_output=True, text=True, timeout=120).stdout out = json.loads(re.search(r'
(.*?)', dom, re.S).group(1) .replace(""", '"').replace("&", "&")) assert out["dateSplit"] == [] # the date is the X axis assert out["holdings"] == {"x": ["currency"], "split": []}