"""U3 — the declared embed seam, in a real DOM. The Python suite can prove what the server OFFERS. It cannot prove where a fragment lands, whether the author's own markup survived the mount, or whether four radio groups scattered down a report still submit as one POST — and that last one is the operator's most important workflow. Before U3 those properties were true by construction, because the server did the placing and the `form=` bindings were static by the time the page was parsed. Now they are true because `/_booth/embed.js` does it in a live document, which is a different kind of claim and needs a different kind of test. So: a real uvicorn on an ephemeral port, a real Chromium. SKIPS, NEVER FAILS, when playwright or the shared browser is unavailable. The box-wide store at /opt/ms-playwright pins specific Chromium revisions and a playwright release that wants a newer one dies with an opaque "Executable doesn't exist" — see pyproject's version bound. A test layer that goes red for an environment reason teaches nothing and trains people to ignore it. """ import json import socket import threading import time import pytest from booth.app import create_app playwright_api = pytest.importorskip( "playwright.sync_api", reason="playwright is not installed" ) @pytest.fixture(scope="module") def browser(): with playwright_api.sync_playwright() as pw: try: b = pw.chromium.launch() except Exception as exc: # noqa: BLE001 - any launch failure is a skip pytest.skip(f"no usable chromium: {exc}") yield b b.close() @pytest.fixture def live(tmp_path): """A real server, because a browser cannot talk to a TestClient.""" import uvicorn sock = socket.socket() sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] sock.close() app = create_app(tmp_path, ttl_hours=24, start_sweeper=False) config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error") server = uvicorn.Server(config) thread = threading.Thread(target=server.run, daemon=True) thread.start() deadline = time.time() + 10 while not server.started and time.time() < deadline: time.sleep(0.02) if not server.started: pytest.skip("uvicorn did not come up") try: yield f"http://127.0.0.1:{port}", tmp_path finally: server.should_exit = True thread.join(timeout=10) SEAM = '' def _multi(booth): from booth.marks import declare_pick booth.mkdir(parents=True, exist_ok=True) declare_pick(booth, "batch", {"title": "Round one", "questions": [ {"key": "r1", "prompt": "First?", "options": ["keep", "cut"]}, {"key": "r2", "prompt": "Second?", "options": ["keep", "cut"]}, ]}) return booth def _single(booth): from booth.marks import declare_pick booth.mkdir(parents=True, exist_ok=True) declare_pick(booth, "winner", {"prompt": "Which render wins?", "options": ["A — baseline", "B — async"]}) return booth def _open(browser, base, name, html, booth): (booth / "index.html").write_text(html, encoding="utf-8") page = browser.new_page() page.goto(f"{base}/b/{name}/", wait_until="networkidle") return page def _answer(booth): raw = json.loads((booth / ".marks.json").read_text()) return raw["marks"][0].get("answer") # ---- the chrome -------------------------------------------------------------- def test_a_declaring_page_gets_its_chrome_mounted(browser, live): base, data = live b = _single(data / "b") page = _open(browser, base, "b", f"r

R

{SEAM}", b) page.wait_for_selector(".booth-nav-home") assert page.locator("h1").inner_text() == "R" # the report is intact assert page.locator(".booth-nav-home").get_attribute("href").endswith("/") # the favicon question, asked of a parsed document instead of raw text assert page.locator('link[rel="icon"]').count() == 1 page.close() def test_a_page_that_never_declared_the_seam_still_mounts(browser, live): """The appended path: every verbatim booth that predates U3 keeps working without its author touching it.""" base, data = live b = _single(data / "b") page = _open(browser, base, "b", "

OLD

", b) page.wait_for_selector(".bk-ask") assert page.locator("h1").inner_text() == "OLD" assert page.locator(".booth-nav-home").count() == 1 page.close() def test_a_page_that_declares_its_own_icon_keeps_it(browser, live): base, data = live b = _single(data / "b") page = _open( browser, base, "b", f'' f"x{SEAM}", b) page.wait_for_selector(".booth-nav-home") icons = page.locator('link[rel="icon"]') assert icons.count() == 1 assert icons.get_attribute("href").startswith("data:image/png") page.close() # ---- placement --------------------------------------------------------------- REPORT = f"""audition

Three voices

{SEAM} """ def test_each_question_lands_where_the_author_put_it(browser, live): """The 2026-09-09 ruling, enforced in the DOM: the question for a voice sits under that voice, not on another page and not in a pile at the end. Both attribute spellings, because live reports use the older one.""" base, data = live b = _multi(data / "b") page = _open(browser, base, "b", REPORT, b) page.wait_for_selector("#lawson .bk-ask") assert page.locator('#lawson input[name="choice.r1"]').count() == 2 assert page.locator('#jo input[name="choice.r2"]').count() == 2 # nothing spilled to the end of the body: every piece had an anchor assert page.locator("body > .bk-ask").count() == 0 assert page.locator("form#bk-ask-form-batch").count() == 1 page.close() def test_the_authors_wrapper_and_its_contents_survive_the_mount(browser, live): """The live `dfa-concepts` shape — a non-empty styled wrapper carrying the anchor attribute. The regex this replaced matched the opening tag and SUBSTITUTED it, eating the class and orphaning the heading. beforeend keeps both and puts the radios under the heading, which is what the markup says.""" base, data = live b = _multi(data / "b") page = _open(browser, base, "b", ( '
' f"

The one asset that must survive

{SEAM}"), b) page.wait_for_selector(".ask .bk-ask") assert page.locator("div.ask").count() == 1 # class kept assert page.locator(".ask h3").inner_text() == "The one asset that must survive" assert page.locator('.ask input[name="choice.r1"]').count() == 2 # radios inside page.close() def test_an_unplaced_question_is_appended_and_so_is_its_submit(browser, live): """INV-7. A multi-question pick needs EVERY question on submit or the POST is a 400 the operator meets only after filling the form in.""" base, data = live b = _multi(data / "b") page = _open(browser, base, "b", f'
{SEAM}', b) # attached, not visible: the shared
is deliberately empty and so has # no box — the controls that bind to it are what the operator sees. page.wait_for_selector("form#bk-ask-form-batch", state="attached") assert page.locator('input[name="choice.r2"]').count() == 2 # never dropped assert page.locator("form#bk-ask-form-batch").count() == 1 # submittable page.close() def test_a_page_with_no_anchors_gets_the_whole_ask(browser, live): base, data = live b = _multi(data / "b") page = _open(browser, base, "b", f"

x

{SEAM}", b) page.wait_for_selector(".bk-ask") assert page.locator('input[name="choice.r1"]').count() == 2 assert page.locator('input[name="choice.r2"]').count() == 2 page.close() def test_an_anchor_naming_no_mark_is_left_alone(browser, live): """A typo'd id stays visible as the author's own empty element rather than being blanked — and the real ask is still never lost.""" base, data = live b = _multi(data / "b") page = _open(browser, base, "b", f'
{SEAM}', b) page.wait_for_selector(".bk-ask") assert page.locator("#t").inner_html().strip() == "" assert page.locator('input[name="choice.r1"]').count() == 2 page.close() def test_the_tail_follows_payload_order(browser, live): """INV-6. Two picks whose creation order and id order disagree: the page must render them `(created, id)`, the order every other surface reads.""" base, data = live b = _single(data / "b") _multi(b) raw = json.loads((b / ".marks.json").read_text()) for e in raw["marks"]: e["created"] = "2026-09-22T10:00:00.000000-07:00" (b / ".marks.json").write_text(json.dumps(raw)) page = _open(browser, base, "b", f"{SEAM}", b) page.wait_for_selector(".bk-ask") ids = page.eval_on_selector_all("[id^='bk-ask-']", "els => els.map(e => e.id)") batch = [i for i, v in enumerate(ids) if "batch" in v] winner = [i for i, v in enumerate(ids) if "winner" in v] assert batch and winner, ids # identical `created`, so the id is the tie-break: batch before winner, # every fragment of one ahead of every fragment of the other. assert max(batch) < min(winner), ids page.close() # ---- the one that actually matters ------------------------------------------ def test_a_form_scattered_down_the_report_submits_every_question(browser, live): """THE load-bearing browser test. Four radio groups under four different artifacts, one somewhere else entirely, bound only by the HTML5 `form=` attribute — and now inserted into a live document in visual order, which means a control can land before the form it points at. If form-owner resolution does not survive that, the operator fills the whole thing in and gets a 400. It was true by construction before U3 (static HTML, resolved at parse). It is true by measurement now. That is the trade this test pays for. """ base, data = live b = _multi(data / "b") page = _open(browser, base, "b", REPORT, b) page.wait_for_selector("#lawson .bk-ask") page.check('#lawson input[name="choice.r1"][value="keep"]') page.check('#jo input[name="choice.r2"][value="cut"]') with page.expect_navigation(): page.click("button.bk-ask-go") ans = _answer(b) assert ans is not None, "the scattered form submitted nothing" assert ans["answers"]["r1"]["choice"] == "keep" assert ans["answers"]["r2"]["choice"] == "cut", \ "a question bound by form= did not reach the POST" assert ans["complete"] is True page.close() def test_the_chip_jumps_to_the_first_fragment_of_the_open_ask(browser, live): base, data = live b = _multi(data / "b") page = _open(browser, base, "b", REPORT, b) page.wait_for_selector(".booth-nav-asks") chip = page.locator(".booth-nav-asks") assert chip.inner_text() == "? 1 open ask" target = chip.get_attribute("href") assert target.startswith("#bk-ask-batch") assert page.locator(target).count() == 1 page.close() def test_the_chip_does_not_jump_to_a_mark_that_merely_shares_a_prefix(browser, live): """A cold panel read the chip rule as a bare prefix match and pointed out that `bk-ask-batch2-...` starts with `bk-ask-batch`. It does not match: the rule is the id EXACTLY, or the id followed by a hyphen. An author element can collide too, so the fixture plants one.""" from booth.marks import declare_pick base, data = live b = _multi(data / "b") declare_pick(b, "batch2", {"prompt": "Unrelated?", "options": ["x", "y"]}) page = _open(browser, base, "b", f'
{SEAM}', b) page.wait_for_selector(".booth-nav-asks") target = page.locator(".booth-nav-asks").get_attribute("href") assert target != "#bk-ask-batchX" landed = page.locator(target) assert landed.count() == 1 # whatever it points at belongs to `batch` itself, not to `batch2` assert "batch2" not in target assert landed.locator('input[name^="choice."]').count() > 0 or "batch" in target page.close() def test_the_canonical_attribute_wins_when_both_are_present(browser, live): """`data-booth-mark` is canonical and `data-booth-ask` is the kept alias. An element carrying both is not a case any live report has, but the precedence has to be decided somewhere rather than by selector order.""" base, data = live b = _multi(data / "b") page = _open(browser, base, "b", ( '
{SEAM}'), b) page.wait_for_selector("#a .bk-ask") assert page.locator('#a input[name="choice.r2"]').count() == 2 # canonical assert page.locator('#a input[name="choice.r1"]').count() == 0 # alias ignored # r1 was never placed, so INV-7 still puts it somewhere assert page.locator('input[name="choice.r1"]').count() == 2 page.close() def test_the_chip_count_comes_from_the_server(browser, live): """INV-4. A half-answered multi-question pick is STILL OPEN, and the page does not get to have an opinion about that — `open_marks` decides.""" from booth.marks import answer_pick base, data = live b = _multi(data / "b") answer_pick(b, "batch", {"r1": "keep"}) page = _open(browser, base, "b", REPORT, b) page.wait_for_selector(".bk-ask") assert page.locator(".booth-nav-asks").count() == 1 answer_pick(b, "batch", {"r1": "keep", "r2": "cut"}) page.reload(wait_until="networkidle") page.wait_for_selector(".bk-ask") assert page.locator(".booth-nav-asks").count() == 0 page.close()