"""S5b of the anti-slop fix slices (docs/contracts/as_antislop.contract.md): the in-place client's focus, its status line, and the unsent-draft guard. Measured in the same real uvicorn + Chromium harness as test_flow_browser. Skips, never fails, when no usable Chromium exists. No test here hears a screen reader: what is held is the precondition (a displayed live region whose text changes), not the announcement. """ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from test_flow_browser import PNG, _picks, _set, browser, live # noqa: E402,F401 (fixtures) WIDE = {"width": 1400, "height": 900} # Every POST waits for the test: window.__release() lets the oldest one through # to the server, window.__release('fail') answers it 500 without sending it. HOLD = """ (function () { var real = window.fetch; window.__held = []; window.fetch = function (u, o) { if (!(o && o.method === 'POST')) return real.apply(this, arguments); var self = this, args = arguments; return new Promise(function (res, rej) { window.__held.push(function (how) { if (how === 'fail') res(new Response('', {status: 500})); else real.apply(self, args).then(res, rej); }); }); }; window.__release = function (how) { var f = window.__held.shift(); if (f) f(how); return !!f; }; })(); """ # Counts swaps, and marks the document so a reload is visible. COUNT = """ document.addEventListener('booth:swapped', function () { window.__swaps = (window.__swaps || 0) + 1; }); """ LINE = '[data-region="status"]' def _page(browser, base, path, hold=False, viewport=WIDE): page = browser.new_page(viewport=viewport) page.add_init_script(COUNT) if hold: page.add_init_script(HOLD) page.goto(f"{base}{path}", wait_until="networkidle") page.evaluate("window.__same = 1") return page def _said(page): return page.evaluate("""() => { const s = document.querySelector('[data-region="status"]'); return {text: s.textContent, tone: s.getAttribute('data-tone')}; }""") def _wait_said(page, needle, timeout=10000): page.wait_for_function( "n => document.querySelector('[data-region=\"status\"]').textContent.includes(n)", arg=needle, timeout=timeout) def _wait_swaps(page, n, timeout=10000): page.wait_for_function("n => (window.__swaps || 0) >= n", arg=n, timeout=timeout) def _wait_held(page, n=1, timeout=5000): page.wait_for_function("n => window.__held.length >= n", arg=n, timeout=timeout) def _focused(page, selector, index=0): return page.evaluate( "([s, i]) => document.activeElement === document.querySelectorAll(s)[i]", [selector, index]) # ---- G1: focus survives a swap ------------------------------------------------------- def test_focus_returns_to_the_pressed_control(browser, live): base, root = live b = _set(root, 3) page = _page(browser, base, "/b/g/") flag = 'figure.item[data-item="02.png"] .flagtoggle button' page.locator(flag).focus() y = page.evaluate("scrollY") page.keyboard.press("Enter") page.wait_for_selector('figure.item.is-flagged[data-item="02.png"]', timeout=10000) _wait_swaps(page, 1) assert _focused(page, flag), page.evaluate("document.activeElement.outerHTML.slice(0, 120)") assert abs(page.evaluate("scrollY") - y) <= 1 page.locator(".verdict .mark-add textarea").fill("a note") page.locator(".verdict .mark-add button").focus() page.keyboard.press("Enter") _wait_swaps(page, 2) assert _focused(page, ".verdict .mark-add button") page.close() _picks(b, ("a1",)) page = _page(browser, base, "/b/g/marks") page.check('#mark-a1 input[type=radio][value="yes"]') page.locator("#mark-a1 .mark-submit").focus() page.keyboard.press("Enter") page.wait_for_function("document.querySelectorAll('.mark-pick.is-answered').length === 1", timeout=10000) _wait_swaps(page, 1) # answered, the pick's form folds into a closed
: focus goes to the # summary that opens it again, never to assert page.locator("#mark-a1 details.mark-formwrap").get_attribute("open") is None assert _focused(page, "#mark-a1 details.mark-formwrap > summary"), \ page.evaluate("document.activeElement.outerHTML.slice(0, 120)") page.close() def test_focus_picks_the_same_one_of_two(browser, live): from booth.marks import set_flag, write_note base, root = live b = _set(root, 3) set_flag(b, "01.png", True) write_note(b, "01.png", "one") write_note(b, "01.png", "two") page = _page(browser, base, "/b/g/") same = '[data-region="verdict"] a[href="view?f=01.png"]' assert page.locator(same).count() >= 2, "positive control: one region, two links with one key" page.evaluate("s => document.querySelectorAll(s)[1].focus()", same) page.evaluate("document.querySelector('figure.item[data-item=\"03.png\"] form.flagtoggle').requestSubmit()") _wait_swaps(page, 1) assert _focused(page, same, 1), page.evaluate("document.activeElement.outerHTML.slice(0, 120)") page.close() def test_focus_lands_on_the_region_when_the_control_is_gone(browser, live): from booth.marks import write_note base, root = live b = _set(root, 3) write_note(b, "01.png", "gone soon") page = _page(browser, base, "/b/g/") page.locator('figure.item[data-item="01.png"] .item-note .mark-x').focus() page.keyboard.press("Enter") page.wait_for_function( "document.querySelectorAll('figure.item[data-item=\"01.png\"] .item-note').length === 0", timeout=10000) _wait_swaps(page, 1) region = '[data-region="item-01.png"]' assert _focused(page, region), page.evaluate("document.activeElement.tagName") page.evaluate("document.querySelector('figure.item[data-item=\"02.png\"] form.flagtoggle').requestSubmit()") _wait_swaps(page, 2) assert _focused(page, region), "a second swap dropped focus from the region" assert page.evaluate("document.activeElement !== document.body") page.close() def test_focus_restore_does_not_scroll(browser, live): base, root = live _set(root, 30) page = _page(browser, base, "/b/g/", hold=True) flag = 'figure.item[data-item="01.png"] .flagtoggle button' page.locator(flag).focus() page.keyboard.press("Enter") _wait_held(page) page.evaluate("window.scrollTo(0, document.documentElement.scrollHeight)") y = page.evaluate("scrollY") assert y > 400, "positive control: the page is long enough to scroll away" page.evaluate("window.__release()") _wait_swaps(page, 1) assert _focused(page, flag) assert abs(page.evaluate("scrollY") - y) <= 1, "restoring focus scrolled the page" page.close() def test_focus_elsewhere_is_left_alone(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/", hold=True) page.locator('figure.item[data-item="02.png"] .flagtoggle button').focus() page.keyboard.press("Enter") _wait_held(page) away = '.theme button[data-theme-choice="system"]' page.locator(away).focus() page.evaluate("window.__release()") _wait_swaps(page, 1) assert _focused(page, away) page.close() # ---- G2: one status line, where it can be seen ------------------------------------------- def _rows_left(page, stage): return page.evaluate("""s => { const v = document.querySelector('.viewer'), st = document.querySelector(s); const others = [...v.children].filter(c => c !== st && getComputedStyle(c).display !== 'none' && getComputedStyle(c).position !== 'absolute' && getComputedStyle(c).position !== 'fixed'); const hs = others.map(c => c.getBoundingClientRect().height); const vh = v.getBoundingClientRect().height; return {stage: st.getBoundingClientRect().height, left: vh - hs.reduce((a, h) => a + h, 0), tallest_other: Math.max(...hs), viewer: vh}; }""", stage) def test_the_status_line_is_visible_on_the_review(browser, live): base, root = live _set(root, 3) for path, stage, press in (("/b/g/view?f=01.png", ".review-body", "#rail form.vflag button"), ("/b/g/compare?a=01.png&b=02.png", ".cmp-body", '[data-region="flag-a"] button')): page = browser.new_page(viewport={"width": 1280, "height": 800}) page.route("**/b/g/flag", lambda route: route.fulfill(status=500, body="")) page.goto(f"{base}{path}", wait_until="networkidle") rows = _rows_left(page, stage) # the stage keeps the 1fr row: it takes the rest, and no other row grew assert abs(rows["stage"] - rows["left"]) <= 1 and rows["tallest_other"] < rows["viewer"] / 4, (path, rows) page.locator(press).first.click() _wait_said(page, "Could not save in place", timeout=5000) hit = page.evaluate("""() => { const s = document.querySelector('[data-region="status"]'); const r = s.getBoundingClientRect(); const e = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2); return e === s || s.contains(e); }""") assert hit, f"{path}: the stage covers the line" page.close() def test_a_save_does_not_move_the_page(browser, live): """Every save now says something. A line that took space in the page flow moved the page under the reader (booth-dev's test_a_flag_lands_in_place_and_every_region_catches_up caught 50px). The words must be in view where the reader is, and move nothing.""" base, root = live _set(root, 30) page = _page(browser, base, "/b/g/", hold=True, viewport={"width": 1400, "height": 800}) tile = 'figure.item[data-item="15.png"]' page.locator(tile).scroll_into_view_if_needed() top = lambda: page.locator(tile).evaluate("e => e.getBoundingClientRect().top") # noqa: E731 before = top() page.locator(f"{tile} .flagtoggle button").click() _wait_held(page) during = top() seen = page.evaluate("""() => { const r = document.querySelector('[data-region="status"]').getBoundingClientRect(); return r.top >= 0 && r.bottom <= innerHeight && r.height > 0; }""") page.evaluate("window.__release()") _wait_said(page, "Saved.") after = top() page.close() assert abs(during - before) <= 1 and abs(after - before) <= 1, (before, during, after) assert seen, "the words are out of view where the reader is" def test_the_covered_letterhead_leaves_the_tab_order(browser, live): base, root = live _set(root, 3) for path in ("/b/g/view?f=01.png", "/b/g/compare?a=01.png&b=02.png"): for vp, want in (({"width": 1280, "height": 800}, "hidden"), ({"width": 390, "height": 844}, "visible")): page = browser.new_page(viewport=vp) page.goto(f"{base}{path}", wait_until="networkidle") got = page.evaluate("""() => ['header.topbar', 'footer.foot'].map( s => getComputedStyle(document.querySelector(s)).visibility)""") assert got == [want, want], (path, vp, got) page.close() # ---- G4: the line speaks in time ------------------------------------------------------------ def test_the_status_line_is_always_displayed(browser, live): base, root = live _set(root, 2) for path in ("/b/g/", "/b/g/view?f=01.png"): page = _page(browser, base, path, hold=True) shown = "() => { const c = getComputedStyle(document.querySelector('[data-region=\"status\"]')); return [c.display, c.visibility]; }" empty = page.evaluate(shown) page.locator("form.flagtoggle button, #rail form.vflag button").first.click() _wait_said(page, "Saving") full = page.evaluate(shown) page.close() for d, v in (empty, full): assert d != "none" and v == "visible", (path, empty, full) def test_an_empty_status_line_takes_no_space(browser, live): base, root = live _set(root, 2) for path in ("/b/g/", "/b/g/view?f=01.png", "/b/g/compare?a=01.png&b=02.png"): page = _page(browser, base, path) h = page.evaluate("document.querySelector('[data-region=\"status\"]').getBoundingClientRect().height") page.close() assert h == 0, (path, h) def test_a_save_says_saving_then_saved(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_held(page) assert _said(page) == {"text": "Saving…", "tone": None} page.evaluate("window.__release()") _wait_said(page, "Saved.") assert _said(page) == {"text": "Saved.", "tone": None} page.wait_for_timeout(2500) assert _said(page) == {"text": "", "tone": None} page.close() def test_a_new_save_is_not_cleared_by_the_last_ones_timer(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/", hold=True) page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_held(page) page.evaluate("window.__release()") _wait_said(page, "Saved.") page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() _wait_held(page) page.wait_for_timeout(2500) assert _said(page)["text"] == "Saving…", "the last save's timer cleared the new one's words" page.evaluate("window.__release()") _wait_said(page, "Saved.") page.close() def test_a_queued_save_keeps_saying_saving(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/", hold=True) page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() _wait_held(page) page.evaluate("window.__release()") _wait_swaps(page, 1) assert _said(page)["text"] == "Saving…", "the first save said Saved. while the second was queued" _wait_held(page) page.evaluate("window.__release()") _wait_said(page, "Saved.") page.close() def test_a_batch_speaks_too(browser, live): base, root = live b = _set(root, 1) _picks(b) page = _page(browser, base, "/b/g/marks", hold=True) page.check('#mark-a1 input[type=radio][value="yes"]') page.check('#mark-a3 input[type=radio][value="no"]') page.locator("#mark-a1 .mark-submit").click() _wait_held(page) assert _said(page) == {"text": "Saving…", "tone": None} page.locator("#mark-a2 .mark-submit").click() assert _said(page)["text"] == "Still saving…" page.evaluate("window.__release()") _wait_held(page) page.evaluate("window.__release()") _wait_said(page, "Saved.") assert _said(page)["tone"] is None page.close() def test_a_repeat_press_says_still_saving(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) btn = 'figure.item[data-item="01.png"] .flagtoggle button' page.locator(btn).click() _wait_held(page) page.locator(btn).click() assert _said(page)["text"] == "Still saving…" page.wait_for_timeout(2500) assert _said(page)["text"] == "Still saving…", "it cleared while the save was still held" assert page.evaluate("window.__held.length") == 1, "the repeat press sent a second POST" page.evaluate("window.__release()") _wait_said(page, "Saved.") page.close() def test_the_form_in_flight_is_busy(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) form = 'figure.item[data-item="01.png"] form.flagtoggle' page.locator(f"{form} button").click() _wait_held(page) assert page.locator(form).get_attribute("aria-busy") == "true" page.evaluate("window.__release()") _wait_swaps(page, 1) assert page.locator(form).get_attribute("aria-busy") is None # the failure path that stays (another draft is on the page): busy ends too page.locator(".verdict .mark-add textarea").fill("another draft") page.locator(f"{form} button").click() _wait_held(page) page.evaluate("window.__release('fail')") _wait_said(page, "Could not save in place") assert page.locator(form).get_attribute("aria-busy") is None page.close() def test_a_failure_then_an_edit_then_a_save(browser, live): """A request trace: a failure that stays, an edit, a second press.""" from booth.marks import marks_for base, root = live b = _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) page.locator(".verdict .mark-add textarea").fill("keep me") tile = 'figure.item[data-item="01.png"]' page.locator(f"{tile} .item-addnote summary").click() page.locator(f"{tile} .item-addnote textarea").fill("first") page.locator(f"{tile} .item-addnote button").click() _wait_held(page) page.evaluate("window.__release('fail')") _wait_said(page, "Could not save in place. Reload to see what was saved") page.wait_for_timeout(2500) said = _said(page) assert said["tone"] == "warn" and "Could not save" in said["text"], said page.locator(f"{tile} .item-addnote textarea").fill("first, edited") page.locator(f"{tile} .item-addnote button").click() _wait_held(page) assert _said(page) == {"text": "Saving…", "tone": None} page.evaluate("window.__release()") _wait_said(page, "Saved.") same = page.evaluate("window.__same === 1") kept = page.locator(".verdict .mark-add textarea").input_value() page.close() assert same and kept == "keep me", (same, kept) assert [m.text for m in marks_for(b) if m.shape == "note"] == ["first, edited"] # ---- G13: leaving with an unsent draft asks first ---------------------------------------------- def _dialogs(page): seen = [] page.on("dialog", lambda d: (seen.append(d.type), d.dismiss())) return seen def test_leaving_with_a_draft_asks(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/view?f=01.png") seen = _dialogs(page) page.locator("#vnote-text").fill("draft") page.locator('.film a.film-f[href="?f=02.png"]').click(no_wait_after=True) page.wait_for_timeout(600) assert seen == ["beforeunload"], seen assert "f=01.png" in page.url and page.locator("#vnote-text").input_value() == "draft" page.evaluate("document.activeElement.blur()") page.keyboard.press("ArrowRight") page.wait_for_timeout(600) assert seen == ["beforeunload", "beforeunload"], seen assert "f=01.png" in page.url page.close() def test_leaving_a_clean_page_does_not_ask(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/view?f=01.png") seen = _dialogs(page) page.locator('.film a.film-f[href="?f=02.png"]').click() page.wait_for_url("**f=02.png", timeout=5000) page.close() assert seen == [] def test_a_saved_draft_no_longer_asks(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/view?f=01.png") seen = _dialogs(page) page.locator("#vnote-text").fill("saved") page.locator("form:has(#vnote-text) button[type=submit]").click() _wait_swaps(page, 1) page.locator('.film a.film-f[href="?f=02.png"]').click() page.wait_for_url("**f=02.png", timeout=5000) page.close() assert seen == [] def test_a_failed_save_keeps_the_other_drafts(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/") page.route("**/b/g/flag", lambda route: route.fulfill(status=500, body="")) page.locator(".verdict .mark-add textarea").fill("other") page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_said(page, "Could not save in place. Reload to see what was saved; your other entries are still here.") page.wait_for_timeout(1500) said = _said(page) same = page.evaluate("window.__same === 1") kept = page.locator(".verdict .mark-add textarea").input_value() page.close() assert same and kept == "other" and said["tone"] == "warn", (same, kept, said) def test_a_failed_save_keeps_text_typed_while_it_flew(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) box = ".verdict .mark-add textarea" page.locator(box).fill("first") page.locator(".verdict .mark-add button").click() _wait_held(page) page.locator(box).fill("first and more") page.evaluate("window.__release('fail')") _wait_said(page, "Could not save in place") page.wait_for_timeout(1500) same = page.evaluate("window.__same === 1") kept = page.locator(box).input_value() page.close() assert same and kept == "first and more", (same, kept) def test_a_changed_page_keeps_the_other_drafts(browser, live): base, root = live b = _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) page.locator(".verdict .mark-add textarea").fill("other") page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_held(page) (b / "03.png").write_bytes(PNG) # a region the live page does not have page.evaluate("window.__release()") _wait_said(page, "Saved. The page changed meanwhile; reload to see it.") page.wait_for_timeout(1500) busy = page.locator('figure.item[data-item="01.png"] form.flagtoggle').get_attribute("aria-busy") assert busy is None, "a settled save left its form busy" said = _said(page) same = page.evaluate("window.__same === 1") kept = page.locator(".verdict .mark-add textarea").input_value() page.close() assert same and kept == "other" and said["tone"] == "warn", (same, kept, said) def test_a_draft_typed_during_the_beat_stays(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/") page.route("**/b/g/flag", lambda route: route.fulfill(status=500, body="")) page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_said(page, "reloading to show what was saved") page.locator(".verdict .mark-add textarea").fill("typed in the beat") page.wait_for_timeout(1500) same = page.evaluate("window.__same === 1") kept = page.locator(".verdict .mark-add textarea").input_value() said = _said(page) page.close() assert same and kept == "typed in the beat", (same, kept) assert "your other entries are still here" in said["text"], said def test_its_own_reload_does_not_ask(browser, live): base, root = live _set(root, 2) page = _page(browser, base, "/b/g/") seen = _dialogs(page) page.route("**/b/g/note", lambda route: route.fulfill(status=500, body="")) page.locator(".verdict .mark-add textarea").fill("sent, and failed") page.locator(".verdict .mark-add button").click() _wait_said(page, "reloading to show what was saved") page.wait_for_function("window.__same !== 1", timeout=5000) page.close() assert seen == [], seen def test_embed_submit_does_not_ask(browser, live): from booth.marks import declare_pick, marks_for base, root = live b = root / "r" b.mkdir() declare_pick(b, "winner", {"prompt": "Which render wins?", "options": ["A", "B"]}) (b / "index.html").write_text( '' '

Report

') page = browser.new_page() page.goto(f"{base}/b/r/", wait_until="networkidle") page.wait_for_selector(".bk-ask-opt input[type=radio]", timeout=10000) seen = _dialogs(page) page.locator(".bk-ask-opt input[type=radio]").first.check() with page.expect_navigation(timeout=10000): page.locator(".bk-ask-go").click() page.close() assert seen == [] assert [m.answer for m in marks_for(b)][0], "the plain submit did not land" def test_embed_leaving_with_a_draft_asks(browser, live): """The positive control for the embed's guard: a changed answer, then the home chip, asks.""" from booth.marks import declare_pick base, root = live b = root / "r" b.mkdir() declare_pick(b, "winner", {"prompt": "Which render wins?", "options": ["A", "B"]}) (b / "index.html").write_text( '' '

Report

') page = browser.new_page() page.goto(f"{base}/b/r/", wait_until="networkidle") page.wait_for_selector(".bk-ask-opt input[type=radio]", timeout=10000) seen = _dialogs(page) page.locator(".bk-ask-opt input[type=radio]").first.check() page.locator(".booth-nav-home").click(no_wait_after=True) page.wait_for_timeout(600) page.close() assert seen == ["beforeunload"], seen # ---- folded from the heid bug-hunt (HNITTA, thread 01M3MRTNTWEPJHTN4APRR81KH4) ------------- def test_a_stale_tile_is_not_left_busy(browser, live): """R1: a save whose own tile survives the swap as a stale tile (un-flagged under the flagged filter) settles like any other: its form is not busy.""" from booth.marks import set_flag base, root = live b = _set(root, 2) set_flag(b, "01.png", True) set_flag(b, "02.png", True) page = _page(browser, base, "/b/g/?filter=flagged") form = 'figure.item[data-item="01.png"] form.flagtoggle' page.locator(f"{form} button").click() page.wait_for_selector('figure.item.is-stale[data-item="01.png"]', timeout=10000) page.wait_for_timeout(300) assert page.locator(form).get_attribute("aria-busy") is None page.close() def test_a_queued_form_is_busy_on_the_live_page(browser, live): """R1's twin: an earlier save's swap replaces a queued form's node, and the live copy is the one that must say it is busy.""" base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() _wait_held(page) page.locator(".verdict .mark-add textarea").fill("queued") page.locator(".verdict .mark-add button").click() page.evaluate("window.__release()") _wait_swaps(page, 1) assert page.locator(".verdict form.mark-add").get_attribute("aria-busy") == "true" _wait_held(page) page.evaluate("window.__release()") _wait_swaps(page, 2) page.wait_for_timeout(200) assert page.locator(".verdict form.mark-add").get_attribute("aria-busy") is None page.close() def test_a_resubmit_in_the_beat_is_not_reloaded_away(browser, live): """R2: the failure's reload is due, and the operator presses again inside the beat. The new save owns the page; the old reload must not abort it.""" from booth.marks import marks_for base, root = live b = _set(root, 2) page = _page(browser, base, "/b/g/") calls = {"n": 0} def first_fails(route): calls["n"] += 1 if calls["n"] == 1: route.fulfill(status=500, body="") else: route.continue_() page.route("**/b/g/note", first_fails) page.locator(".verdict .mark-add textarea").fill("again") page.locator(".verdict .mark-add button").click() _wait_said(page, "reloading to show what was saved") page.locator(".verdict .mark-add button").click() _wait_said(page, "Saved.") page.wait_for_timeout(1500) same = page.evaluate("window.__same === 1") page.close() assert same, "the old failure's reload fired over the new save" assert [m.text for m in marks_for(b) if m.shape == "note"] == ["again"] def test_an_unrelated_save_does_not_bury_a_failure(browser, live): """R4: a note fails and stays; a different save then lands. The line keeps saying the note is not saved, rather than "Saved.".""" base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) page.locator(".verdict .mark-add textarea").fill("another draft") # so the failure stays tile = 'figure.item[data-item="01.png"]' page.locator(f"{tile} .item-addnote summary").click() page.locator(f"{tile} .item-addnote textarea").fill("did not go") page.locator(f"{tile} .item-addnote button").click() _wait_held(page) page.evaluate("window.__release('fail')") _wait_said(page, "Could not save in place. Reload to see what was saved") page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() _wait_held(page) page.evaluate("window.__release()") _wait_swaps(page, 1) page.wait_for_timeout(300) said = _said(page) page.close() assert said["tone"] == "warn" and "Could not save in place" in said["text"], said def test_a_save_whose_page_would_not_refresh_says_saved(browser, live): """R8: the POST lands (204) and the page GET fails. That is a save, and the line must not say it could not save (a second press would write it twice).""" from booth.marks import marks_for base, root = live b = _set(root, 2) page = _page(browser, base, "/b/g/") gate = {"on": False} page.route("**/b/g/", lambda route: route.fulfill(status=500, body="") if gate["on"] and route.request.method == "GET" else route.continue_()) page.locator(".verdict .mark-add textarea").fill("landed") tile = 'figure.item[data-item="01.png"]' page.locator(f"{tile} .item-addnote summary").click() page.locator(f"{tile} .item-addnote textarea").fill("another draft") gate["on"] = True page.locator(".verdict .mark-add button").click() _wait_said(page, "Saved. Could not refresh the page; reload to see it.") page.wait_for_timeout(1500) said = _said(page) same = page.evaluate("window.__same === 1") page.close() assert said["tone"] == "warn" and same, (said, same) assert [m.text for m in marks_for(b) if m.shape == "note"] == ["landed"] def test_focus_on_a_summary_survives_the_next_swap(browser, live): """R10: focus that was restored to an answered pick's summary stays there through the next swap.""" base, root = live b = _set(root, 1) _picks(b, ("a1", "a2")) page = _page(browser, base, "/b/g/marks") page.check('#mark-a1 input[type=radio][value="yes"]') page.locator("#mark-a1 .mark-submit").focus() page.keyboard.press("Enter") _wait_swaps(page, 1) summary = "#mark-a1 details.mark-formwrap > summary" assert _focused(page, summary) page.evaluate("document.querySelector('#mark-a2 input[type=radio][value=\"no\"]').checked = true;" "document.querySelector('#mark-a2 form.mark-form').requestSubmit()") _wait_swaps(page, 2) assert _focused(page, summary), page.evaluate("document.activeElement.outerHTML.slice(0, 120)") page.close() def test_an_edit_made_while_saving_is_not_called_saved(browser, live): """R12: text added to a note while it was saving is on screen and not on the server. The line says so, instead of "Saved.".""" base, root = live _set(root, 2) page = _page(browser, base, "/b/g/", hold=True) box = ".verdict .mark-add textarea" page.locator(box).fill("hello") page.locator(".verdict .mark-add button").click() _wait_held(page) page.locator(box).fill("hello!") page.evaluate("window.__release()") _wait_swaps(page, 1) page.wait_for_timeout(300) said = _said(page) kept = page.locator(box).input_value() page.close() assert kept == "hello!", kept assert said["tone"] == "warn" and "not saved yet" in said["text"], said def _embed_page(browser, base, root, host_script=""): from booth.marks import declare_pick b = root / "r" b.mkdir() declare_pick(b, "winner", {"prompt": "Which render wins?", "options": ["A", "B"]}) (b / "index.html").write_text( '' f'

Report

{host_script}') page = browser.new_page() page.goto(f"{base}/b/r/", wait_until="networkidle") page.wait_for_selector(".bk-ask-opt input[type=radio]", timeout=10000) return page def test_embed_a_cancelled_submit_is_guarded_again(browser, live): """R3: a host handler cancels our native submit. Nothing was sent, so the answer is an unsent draft again, and leaving asks.""" base, root = live host = ("") page = _embed_page(browser, base, root, host) seen = _dialogs(page) page.locator(".bk-ask-opt input[type=radio]").first.check() page.locator(".bk-ask-go").click() page.wait_for_timeout(300) assert "/b/r/" in page.url and seen == [], "positive control: the host cancelled it" page.locator(".booth-nav-home").click(no_wait_after=True) page.wait_for_timeout(600) page.close() assert seen == ["beforeunload"], seen def test_embed_the_skip_covers_one_leave(browser, live): """R3: the skip for our own submit covers the one navigation it started. A submit whose response does not replace the page (a 204 here; a stop or Esc in life) leaves the answer unsent on screen, and the next leave asks.""" base, root = live page = _embed_page(browser, base, root) page.route("**/b/r/answer", lambda route: route.fulfill(status=204, body="")) seen = _dialogs(page) page.locator(".bk-ask-opt input[type=radio]").first.check() page.locator(".bk-ask-go").click() page.wait_for_timeout(500) assert "/b/r/" in page.url and seen == [], "positive control: the page stayed, and our submit did not ask" page.locator(".booth-nav-home").click(no_wait_after=True) page.wait_for_timeout(600) page.close() assert seen == ["beforeunload"], seen