"""The plan's rotation editor, driven in a real browser. The editor is chips over a hidden textarea, which is still the field that submits, so every change has to land in that field, in order, repeats and all. Its tests read the page's markup; none pressed a button. So a seeded walk here adds, moves, drags, removes and edits as text, and after every step the chips, the field, the count, the numbering and the arrows must all match a plain list kept beside them. The preview's answers arrive late and out of order, and the table must end on the rotation as it stands. A press on a palette chip that wobbled near the list added the ticker twice: the drop placed one, and the click that ends every press added another. """ 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 make_login pytestmark = pytest.mark.visual TICKERS = ["ALPHA", "BETAX", "GAMMA", "OMEGA", "ZULU", "VAS", "IVV", "NDQ", "A200"] # The preview's answers come back after a delay, `DELAY`, a function of the # request's number and a seeded draw; an abort is honoured, as a real fetch # honours it. PREVIEW = """ """ # Often slower than the editor's 250ms debounce, so requests overlap. SOMETIMES_SLOW = "Math.floor(rand() * 600)" WALK = """ """ def _page(client, session_factory) -> str: make_login(client, session_factory) with session_factory() as s: for ticker in TICKERS: fac.hold(s, fac.make_instrument(s, ticker)) s.commit() page = client.get("/schedule?edit=1", headers=HTML) assert page.status_code == 200 return page.text def _chrome(target, *extra) -> str: return subprocess.run( [CHROME, "--headless", "--disable-gpu", "--dump-dom", "--no-sandbox", "--allow-file-access-from-files", "--window-size=1400,1000", *extra, "--virtual-time-budget=600000", target.as_uri()], capture_output=True, text=True, timeout=180).stdout def _result(dom: str): 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_every_step_lands_in_the_field_in_order(client, session_factory, tmp_path, seeds): target = tmp_path / "plan.html" driver = (WALK.replace("SEEDS", json.dumps(seeds)).replace("STEPS", "40") .replace("TICKERS", json.dumps(TICKERS))) target.write_text(_standalone(_page(client, session_factory), driver) .replace("", "" + PREVIEW.replace("DELAY", SOMETIMES_SLOW), 1), encoding="utf-8") runs = _result(_chrome(target, "--force-prefers-reduced-motion")) for run in runs: assert run["faults"] == [], (run["seed"], run["faults"][:2], run["log"]) # The newest rotation is the one previewed, whatever order the # answers came back in. assert run["lastAsked"] == ", ".join(run["final"]), run["seed"] assert run["table"] == run["final"], run["seed"] done = [line.split()[0] for run in runs for line in run["log"]] assert {"add", "drop", "up", "down", "remove", "drag", "text"} <= set(done) NEAR_THE_LIST = """ """ def test_a_click_that_wobbles_near_the_list_adds_once(client, session_factory, tmp_path): target = tmp_path / "plan.html" target.write_text(_standalone(_page(client, session_factory), NEAR_THE_LIST) .replace("", "" + PREVIEW.replace("DELAY", "0"), 1), encoding="utf-8") out = _result(_chrome(target)) assert out["near"], "no palette chip sits within reach of the list at this width" assert out["added"] == [out["ticker"]] # The page's own first preview is slow and the one after a click quick, so the # first lands last unless it was abandoned. RACE = """ """ def test_a_preview_that_arrives_late_does_not_replace_a_newer_one(client, session_factory, tmp_path): target = tmp_path / "plan.html" target.write_text(_standalone(_page(client, session_factory), RACE) .replace("", "" + PREVIEW.replace("DELAY", "n === 0 ? 800 : 10"), 1), encoding="utf-8") out = _result(_chrome(target, "--force-prefers-reduced-motion")) assert len(out["asked"]) == 2, "the click did not ask while the first was out" assert out["table"] == out["chips"]