"""S5c of the anti-slop fix slices (docs/contracts/as_antislop.contract.md): whose key it is, the doc bar, tile sizes, the rail's shadow, reveal names, and a refused batch that an unrelated save must not bury. Measured in the same real uvicorn + Chromium harness as test_flow_browser. Skips, never fails, when no usable Chromium exists. """ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from test_flow_browser import PNG, _picks, _png, _set, browser, live # noqa: E402,F401 (fixtures) from test_antislop_s5b_browser import COUNT, WIDE, _said, _wait_said, _wait_swaps # noqa: E402 PHONE = {"width": 390, "height": 844} _RECT = "e => { const b = e.getBoundingClientRect(); return {l: b.left, r: b.right, t: b.top, b: b.bottom}; }" def _page(browser, base, path, viewport=WIDE, js=True): ctx = browser.new_context(viewport=viewport, java_script_enabled=js) page = ctx.new_page() if js: page.add_init_script(COUNT) page.goto(f"{base}{path}", wait_until="networkidle") return page def _active(page): return page.evaluate("""() => { const a = document.activeElement; return a === document.body ? 'body' : (a.getAttribute('data-item') || a.id || a.tagName); }""") def _posts(page): seen = [] page.on("request", lambda r: seen.append(r.url) if r.method == "POST" else None) return seen # ---- booth-dev's note: a refused batch is not buried by an unrelated save ------------ def test_a_batch_refusal_outlives_an_unrelated_save(browser, live): """a2's POST is dropped, so the batch says "Not saved: a2" and a2 stays unsent on the page. An unrelated flag then lands. Its "Saved." must not replace the refusal's words, which S5b already guarantees for a one-form failure.""" base, root = live b = _set(root, 2) _picks(b, ("a1", "a2")) page = _page(browser, base, "/b/g/") page.route("**/b/g/answer", lambda route: route.abort() if "ask=a2" in (route.request.post_data or "") else route.continue_()) page.check('.verdict #mark-a1 input[type=radio][value="yes"]') page.check('.verdict #mark-a2 input[type=radio][value="no"]') page.locator(".verdict #mark-a1 .mark-submit").click() _wait_said(page, "Not saved") page.wait_for_function( "document.querySelectorAll('.verdict .mark-pick.is-answered').length === 1", timeout=10000) swaps = page.evaluate("window.__swaps || 0") page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() page.wait_for_selector('figure.item.is-flagged[data-item="02.png"]', timeout=10000) _wait_swaps(page, swaps + 1) page.wait_for_timeout(2500) # past the 2s beat that clears a "Saved." said = _said(page) kept = page.is_checked('.verdict #mark-a2 input[type=radio][value="no"]') page.close() assert kept, "the refused pick's choice was lost" assert said["tone"] == "warn" and "Not saved: a2" in said["text"], said def test_two_standing_failures_are_both_said_once_each(browser, live): """Each failure that stays is said again after an unrelated save, each distinct message once, in the order they failed: a note that could not be saved, then a batch with two refusals.""" base, root = live b = _set(root, 3) _picks(b, ("a1", "a2", "a3")) page = _page(browser, base, "/b/g/") fail = {"note": True} page.route("**/b/g/note", lambda route: route.fulfill(status=500, body="") if fail["note"] else route.continue_()) page.route("**/b/g/answer", lambda route: route.abort() if "ask=a1" not in (route.request.post_data or "") else route.continue_()) page.locator(".verdict .mark-add textarea").fill("a draft that stays") # so no reload 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_said(page, "Could not save in place. Reload to see what was saved") for q, v in (("a1", "yes"), ("a2", "no"), ("a3", "no")): page.check(f'.verdict #mark-{q} input[type=radio][value="{v}"]') page.locator(".verdict #mark-a1 .mark-submit").click() _wait_said(page, "Not saved") at_refusal = _said(page)["text"] # R3: the note's failure is not buried swaps = page.evaluate("window.__swaps || 0") page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() page.wait_for_selector('figure.item.is-flagged[data-item="02.png"]', timeout=10000) _wait_swaps(page, swaps + 1) page.wait_for_timeout(2500) said = _said(page) page.close() assert "Could not save in place" in at_refusal, at_refusal text = said["text"] assert said["tone"] == "warn", said assert text.count("Could not save in place") == 1 and text.count("Not saved: a2") == 1, text assert text.index("Could not save in place") < text.index("Not saved: a2"), text assert "a3" in text, text # ---- G6: a page's keys never take a key the focused element uses --------------------- def _media_set(root): b = root / "g" b.mkdir() for n in ("01.png", "03.png"): (b / n).write_bytes(PNG) (b / "02.webm").write_bytes(b"\x1a\x45\xdf\xa3") return b def test_a_player_keeps_its_keys(browser, live): base, root = live _media_set(root) # the review: a focused player keeps the arrows, the letters and Escape page = _page(browser, base, "/b/g/view?f=02.webm") posts = _posts(page) page.locator("#vmedia").focus() assert _active(page) == "vmedia", "precondition: the player takes focus" for key in ("ArrowRight", "ArrowLeft", "f", "Space"): page.keyboard.press(key) page.wait_for_timeout(600) stayed = page.url.endswith("view?f=02.webm") # ...but not Escape, which a player has no use for outside fullscreen (heid # bug-hunt HRÖSKVA R8): Esc still goes back to the grid with page.expect_navigation(): page.keyboard.press("Escape") back = "/b/g/" in page.url and "view?" not in page.url page.goto(f"{base}/b/g/view?f=02.webm", wait_until="networkidle") with page.expect_navigation(): # the negative control: from the page it moves page.keyboard.press("ArrowRight") moved = page.url.endswith("view?f=03.png") page.close() assert stayed and posts == [], (stayed, posts) assert back, "Escape from a focused player did not go back to the grid" assert moved # the grid: an arrow on a focused tile video is the player's, not the cursor's page = _page(browser, base, "/b/g/") page.locator('figure.item[data-item="02.webm"] video').focus() page.keyboard.press("ArrowRight") page.wait_for_timeout(300) none = page.locator("figure.item.is-cursor").count() page.evaluate("document.activeElement.blur()") page.keyboard.press("ArrowRight") page.wait_for_selector("figure.item.is-cursor", timeout=5000) page.close() assert none == 0 # compare: an arrow on a focused player does not step the pair page = _page(browser, base, "/b/g/compare?a=01.png&b=02.webm") page.locator(".cmp-media").focus() page.keyboard.press("ArrowRight") page.wait_for_timeout(600) kept = "b=02.webm" in page.url page.close() assert kept def test_enter_on_a_control_does_not_open_the_cursor_tile(browser, live): base, root = live _set(root, 3) page = _page(browser, base, "/b/g/") page.keyboard.press("ArrowRight") page.wait_for_selector("figure.item.is-cursor", timeout=5000) summary = page.locator('figure.item[data-item="03.png"] .item-addnote summary') summary.focus() page.keyboard.press("Enter") page.wait_for_timeout(600) stayed = page.url.endswith("/b/g/") opened = page.locator('figure.item[data-item="03.png"] details.item-addnote').get_attribute("open") # nor from any other focused node that is not the page itself (the rail stands in # for one, as an S5b fallback region would be) page.evaluate("() => { const r = document.querySelector('.rail'); r.setAttribute('tabindex', '-1'); r.focus(); }") page.keyboard.press("Enter") page.wait_for_timeout(600) stayed = stayed and page.url.endswith("/b/g/") # the positive control: Enter on the focused cursor tile opens its review page.locator('figure.item[data-item="01.png"]').focus() with page.expect_navigation(): page.keyboard.press("Enter") reviewed = page.url.endswith("view?f=01.png") page.close() assert stayed, "Enter on a summary opened the cursor tile" assert opened is not None, "Enter did not open the summary it was pressed on" assert reviewed def test_a_focused_pannable_stage_pans_with_the_arrows(browser, live): from test_flow_browser import _stage_set base, root = live _stage_set(root, {"a.png": (3000, 2400), "b.png": (400, 300)}) page = _page(browser, base, "/b/g/view?f=a.png") page.locator("#btn-one").click() page.wait_for_selector("#vstage.can-pan", timeout=5000) page.locator("#vstage").focus() assert _active(page) == "vstage", "precondition: Chromium puts a scroller in the Tab order" page.keyboard.press("ArrowRight") page.wait_for_timeout(600) stayed = page.url.endswith("view?f=a.png") panned = page.evaluate("document.getElementById('vstage').scrollLeft") page.evaluate("document.activeElement.blur()") with page.expect_navigation(): page.keyboard.press("ArrowRight") moved = page.url.endswith("view?f=b.png") page.close() assert stayed and panned > 0, (stayed, panned) assert moved def test_the_grid_cursor_is_real_focus(browser, live): base, root = live _set(root, 5) page = _page(browser, base, "/b/g/") page.keyboard.press("ArrowRight") page.wait_for_selector("figure.item.is-cursor", timeout=5000) first = page.evaluate("""() => { const a = document.activeElement; return {cursor: a.classList.contains('is-cursor'), item: a.getAttribute('data-item'), tabbed: [...document.querySelectorAll('figure.item[tabindex]')].map(e => e.getAttribute('data-item')), outline: getComputedStyle(a).outlineStyle}; }""") page.keyboard.press("ArrowRight") second = page.evaluate("""() => { const a = document.activeElement; return {cursor: a.classList.contains('is-cursor'), item: a.getAttribute('data-item'), tabbed: [...document.querySelectorAll('figure.item[tabindex]')].map(e => e.getAttribute('data-item'))}; }""") page.keyboard.press("Escape") after = {"cursors": page.locator("figure.item.is-cursor").count(), "active": _active(page)} page.close() assert first == {"cursor": True, "item": "01.png", "tabbed": ["01.png"], "outline": "none"}, first assert second == {"cursor": True, "item": "02.png", "tabbed": ["02.png"]}, second assert after == {"cursors": 0, "active": "body"}, after def test_focus_returned_to_a_tile_makes_it_the_cursor(browser, live): from booth.marks import write_note base, root = live b = _set(root, 8) write_note(b, "05.png", "withdraw me") page = _page(browser, base, "/b/g/") page.locator('figure.item[data-item="05.png"] .item-note .mark-x').focus() page.keyboard.press("Enter") page.wait_for_function( "!document.querySelector('figure.item[data-item=\"05.png\"] .item-note')", timeout=10000) _wait_swaps(page, 1) landed = page.evaluate("""() => { const a = document.activeElement; return {item: a.getAttribute('data-item'), cursor: a.classList.contains('is-cursor')}; }""") page.keyboard.press("ArrowRight") moved = _active(page) page.close() assert landed == {"item": "05.png", "cursor": True}, landed assert moved == "06.png", moved # ---- G7: the doc bar's controls leave its --------------------------------- def _doc_set(root): b = root / "g" b.mkdir() (b / "notes.md").write_text("# Notes\n\n" + "A line of prose. " * 40 + "\n") for n in ("01.png", "02.png"): (b / n).write_bytes(PNG) return b def test_a_closed_doc_keeps_its_tools(browser, live): base, root = live _doc_set(root) for js in (True, False): page = _page(browser, base, "/b/g/", js=js) doc = 'figure.item-doc[data-item="notes.md"]' page.locator(f"{doc} summary.doc-sum").click() page.wait_for_timeout(200) state = page.evaluate("""d => { const f = document.querySelector(d); const tools = f.querySelector('.doc-tools').getBoundingClientRect(); return {body: f.querySelector('.doc-body').getClientRects().length > 0, tools: tools.width > 0 && tools.height > 0}; }""", doc) assert state == {"body": False, "tools": True}, (js, state) # the body is not rendered if js: page.locator(f"{doc} .doc-tools .flagtoggle button").click() page.wait_for_function( "d => document.querySelector(d + ' .doc-tools .flagtoggle button').textContent.includes('flagged')", arg=doc, timeout=10000) assert page.evaluate("window.__swaps || 0") >= 1, "the flag reloaded instead of landing in place" page.close() def test_the_doc_bar_is_one_row_wide_and_two_on_a_phone(browser, live): base, root = live _doc_set(root) got = {} for label, vp in (("wide", {"width": 1280, "height": 900}), ("phone", PHONE)): page = _page(browser, base, "/b/g/", viewport=vp) doc = 'figure.item-doc[data-item="notes.md"]' got[label] = {"sum": page.locator(f"{doc} summary.doc-sum").evaluate(_RECT), "name": page.locator(f"{doc} .doc-name").evaluate(_RECT), "tools": page.locator(f"{doc} .doc-tools").evaluate(_RECT)} page.close() w, p = got["wide"], got["phone"] assert w["tools"]["t"] < w["sum"]["b"] and w["tools"]["b"] > w["sum"]["t"], w # one row assert p["tools"]["t"] >= p["name"]["b"] - 1, p # under the name def test_a_closed_doc_stays_closed_through_a_save(browser, live): base, root = live _doc_set(root) page = _page(browser, base, "/b/g/") doc = 'figure.item-doc[data-item="notes.md"]' page.locator(f"{doc} summary.doc-sum").click() page.locator('figure.item[data-item="01.png"] .flagtoggle button').click() page.wait_for_selector('figure.item.is-flagged[data-item="01.png"]', timeout=10000) _wait_swaps(page, 1) state = page.evaluate("""d => { const f = document.querySelector(d); return {open: f.querySelector('details.doc-fold').open, body: f.querySelector('.doc-body').getClientRects().length > 0}; }""", doc) page.close() assert state == {"open": False, "body": False}, state # ---- G14: a gallery tile's picture carries its size --------------------------------- def _tall_set(root, n=40): b = root / "g" b.mkdir() for i in range(1, n + 1): (b / f"{i:02d}.png").write_bytes(_png(400, 500 + (i * 37) % 400)) return b def _landing(page, rel): return page.evaluate("""r => ({top: document.getElementById('item-' + r).getBoundingClientRect().top, rail: document.querySelector('.rail').getBoundingClientRect().bottom})""", rel) def test_a_link_to_a_tile_lands_where_it_points(browser, live): """Measured before the fix: the tile's top sat 44px below its mark (3 of 3).""" base, root = live _tall_set(root) page = _page(browser, base, "/b/g/#item-30.png") page.wait_for_timeout(600) got = _landing(page, "30.png") page.close() assert abs(got["top"] - (got["rail"] + 12)) <= 2, got # ---- G15: nothing reached by Tab sits under the sticky rail ------------------------- def test_a_tabbed_control_does_not_hide_under_the_rail(browser, live): """Measured before the fix: the Tab-focused flag button sat at 19.6px, under the rail's bottom at 47.6px.""" base, root = live _tall_set(root, 16) page = _page(browser, base, "/b/g/", viewport={"width": 1400, "height": 700}) btn = 'figure.item[data-item="10.png"] .flagtoggle button' page.evaluate("""s => { const all = [...document.querySelectorAll('a[href],button,summary,input,textarea')] .filter(e => e.offsetParent); const b = document.querySelector(s); all[all.indexOf(b) - 1].focus({preventScroll: true}); window.scrollBy(0, b.getBoundingClientRect().top - 20); }""", btn) page.wait_for_timeout(200) page.keyboard.press("Tab") page.wait_for_timeout(400) got = page.evaluate("""s => ({on: document.activeElement === document.querySelector(s), top: document.activeElement.getBoundingClientRect().top, rail: document.querySelector('.rail').getBoundingClientRect().bottom})""", btn) page.close() assert got["on"], "precondition: Tab reached the flag button" assert got["top"] >= got["rail"], got def test_a_jump_lands_just_below_the_rail(browser, live): """One offset, not two: scroll padding on the page REPLACES the tile's scroll margin, since the two add.""" base, root = live _set(root, 40) page = _page(browser, base, "/b/g/#item-31.png", viewport={"width": 1400, "height": 700}) page.wait_for_timeout(400) got = _landing(page, "31.png") page.close() assert abs(got["top"] - (got["rail"] + 12)) <= 2, got def test_pages_without_a_rail_have_no_scroll_padding(browser, live): base, root = live _set(root, 3) got = {} for path in ("/", "/b/g/view?f=01.png", "/b/g/"): page = _page(browser, base, path) got[path] = page.evaluate("getComputedStyle(document.documentElement).scrollPaddingTop") page.close() assert got["/"] == "auto" and got["/b/g/view?f=01.png"] == "auto", got assert got["/b/g/"] != "auto", got # the positive control def test_the_rail_padding_falls_back_with_scripts_off(browser, live): """With scripts off --rail-h is unset and its fallback stands in: 120px, or the measured worst case at <=480px, 205px. The 12px is added either way.""" base, root = live _set(root, 3) got = {} for label, vp in (("wide", {"width": 1400, "height": 900}), ("phone", PHONE)): page = _page(browser, base, "/b/g/", viewport=vp, js=False) got[label] = page.evaluate("getComputedStyle(document.documentElement).scrollPaddingTop") page.close() assert got == {"wide": "132px", "phone": "217px"}, got # ---- G17: a reveal control's name is the words on it --------------------------------- def _named(page, name): return page.get_by_role("button", name=name, exact=True).count() def test_reveal_names_follow_their_words(browser, live): from booth.app import set_blurred base, root = live b = root / "g" b.mkdir() for rel in ("a.png", "b.png"): (b / rel).write_bytes(_png(40, 30)) set_blurred(b, rel, True) (b / "notes.md").write_text("# Notes\n\nprose\n") set_blurred(b, "notes.md", True) got = {} page = _page(browser, base, "/b/g/") got["tile"] = [_named(page, "reveal a.png")] page.get_by_role("button", name="reveal a.png", exact=True).click() got["tile"] += [_named(page, "hide a.png")] ra = page.locator("[data-reveal-all]") page.mouse.move(1, 1) look = ra.evaluate("e => getComputedStyle(e).borderTopColor") got["all"] = [_named(page, "reveal all — blur is cosmetic")] ra.click() # the pointer leaves first: :hover draws the same border as the "on" look, # and a read under the pointer measured the hover (a vacuous gate run) page.mouse.move(1, 1) page.wait_for_timeout(300) got["all"] += [_named(page, "blur again"), ra.get_attribute("aria-pressed"), ra.evaluate("e => getComputedStyle(e).borderTopColor") != look] page.close() page = _page(browser, base, "/b/g/view?f=a.png") got["review"] = [_named(page, "reveal a.png — blur is cosmetic")] page.locator("#vreveal").click() got["review"] += [_named(page, "hide a.png")] page.close() page = _page(browser, base, "/b/g/compare?a=a.png&b=b.png") got["compare"] = [_named(page, "reveal A — blur is cosmetic")] page.locator('.cmp-reveal[data-side="a"]').click() got["compare"] += [_named(page, "hide A")] page.close() page = _page(browser, base, "/b/g/view?f=notes.md") got["doc"] = [_named(page, "reveal notes.md — blur is cosmetic")] page.locator("#docreveal").click() got["doc"] += [_named(page, "hide notes.md")] page.close() assert got == {"tile": [1, 1], "all": [1, 1, None, True], "review": [1, 1], "compare": [1, 1], "doc": [1, 1]}, got # ---- folded from the heid bug-hunt (HRÖSKVA) ----------------------------------------- def _doc_between(root): b = root / "g" b.mkdir() for n in ("01.png", "03.png", "04.png"): (b / n).write_bytes(PNG) (b / "02-notes.md").write_text("# Notes\n\nprose\n") return b def test_the_cursor_skips_a_closed_doc(browser, live): """R1: a doc closed with its ✕ is display:none, and a cursor that landed on it could not take focus while f and Enter acted on it blind.""" base, root = live _doc_between(root) page = _page(browser, base, "/b/g/") page.locator('figure.item-doc[data-item="02-notes.md"] .doc-close').click() page.evaluate("document.activeElement.blur(); scrollTo(0, 0)") page.keyboard.press("ArrowRight") page.wait_for_selector("figure.item.is-cursor", timeout=5000) page.keyboard.press("ArrowRight") got = page.evaluate("""() => ({active: document.activeElement.getAttribute('data-item'), cursor: [...document.querySelectorAll('figure.item.is-cursor')].map(e => e.getAttribute('data-item')), tabbed: [...document.querySelectorAll('figure.item[tabindex]')].map(e => e.getAttribute('data-item'))})""") page.close() assert got == {"active": "03.png", "cursor": ["03.png"], "tabbed": ["03.png"]}, got def test_n_opens_the_note_on_a_closed_doc(browser, live): """R10: the note field sits in the doc's body, which a closed fold hides.""" base, root = live _doc_between(root) page = _page(browser, base, "/b/g/") page.locator('figure.item-doc[data-item="02-notes.md"] summary.doc-sum').click() page.evaluate("document.activeElement.blur(); scrollTo(0, 0)") page.keyboard.press("ArrowRight") page.keyboard.press("ArrowRight") page.wait_for_selector('figure.item-doc.is-cursor[data-item="02-notes.md"]', timeout=5000) page.keyboard.press("n") got = page.evaluate("""() => { const f = document.querySelector('figure.item-doc[data-item="02-notes.md"]'); return {open: f.querySelector('details.doc-fold').open, typing: document.activeElement === f.querySelector('.item-addnote textarea')}; }""") page.close() assert got == {"open": True, "typing": True}, got def test_enter_reviews_a_file_named_view(browser, live): """R5: Enter followed the first link whose href merely STARTED with "view", and a media tile's download link comes first: views.webm downloaded. A file with no review (viewer.zip) is not downloaded by Enter either.""" base, root = live b = root / "g" b.mkdir() (b / "views.webm").write_bytes(b"\x1a\x45\xdf\xa3") (b / "viewer.zip").write_bytes(b"PK\x05\x06" + b"\x00" * 18) page = _page(browser, base, "/b/g/") downloads = [] page.on("download", lambda d: downloads.append(d.suggested_filename)) page.locator('figure.item[data-item="viewer.zip"]').evaluate("e => e.setAttribute('tabindex', '-1')") page.locator('figure.item[data-item="viewer.zip"]').focus() # the cursor follows focus page.keyboard.press("Enter") page.wait_for_timeout(800) stayed = page.url.endswith("/b/g/") page.locator('figure.item[data-item="views.webm"]').evaluate("e => e.setAttribute('tabindex', '-1')") page.locator('figure.item[data-item="views.webm"]').focus() with page.expect_navigation(): page.keyboard.press("Enter") reviewed = page.url.endswith("view?f=views.webm") page.wait_for_timeout(300) page.close() assert stayed and reviewed and downloads == [], (stayed, reviewed, downloads) def test_the_rail_height_follows_the_live_rail_after_a_save(browser, live): """R2: the rail is a region a save replaces. The height script watched the node it found first, so after a save it measured a detached rail.""" base, root = live _set(root, 6) page = _page(browser, base, "/b/g/") page.locator('figure.item[data-item="02.png"] .flagtoggle button').click() page.wait_for_selector('figure.item.is-flagged[data-item="02.png"]', timeout=10000) _wait_swaps(page, 1) page.wait_for_timeout(300) probe = """() => ({var: getComputedStyle(document.documentElement).getPropertyValue('--rail-h').trim(), live: document.querySelector('.rail').offsetHeight + 'px'})""" after_save = page.evaluate(probe) page.set_viewport_size({"width": 420, "height": 800}) # the rail wraps taller page.wait_for_timeout(400) after_resize = page.evaluate(probe) page.close() assert after_save["var"] == after_save["live"], after_save assert after_resize["var"] == after_resize["live"], after_resize def test_tab_between_rail_links_does_not_move_the_page(browser, live): """R9: the page's scroll padding clears the rail for everything under it, and a control IN the stuck rail is not under it. Measured before: each Tab between group links scrolled the page up 357px (3 of 3).""" base, root = live b = root / "g" b.mkdir() for grp in ("aa", "bb", "cc", "dd", "ee"): for i in range(1, 13): (b / f"{grp}{i:02d}.png").write_bytes(PNG) page = _page(browser, base, "/b/g/", viewport={"width": 1400, "height": 700}) page.evaluate("scrollTo(0, 2000)") page.wait_for_timeout(300) page.evaluate("document.querySelectorAll('.rail-g')[1].focus({preventScroll: true})") y = page.evaluate("scrollY") page.keyboard.press("Tab") page.wait_for_timeout(400) got = {"on": page.evaluate("document.activeElement.className"), "moved": page.evaluate("scrollY") - y} page.close() assert got == {"on": "rail-g", "moved": 0}, got def test_a_later_failure_does_not_bury_an_earlier_one(browser, live): """R3: every warning says the failures still standing first, so a later failure never buries an earlier one: here a batch refusal, then a note.""" base, root = live b = _set(root, 3) _picks(b, ("a1", "a2")) page = _page(browser, base, "/b/g/") page.route("**/b/g/answer", lambda route: route.abort() if "ask=a2" in (route.request.post_data or "") else route.continue_()) page.route("**/b/g/note", lambda route: route.fulfill(status=500, body="")) page.check('.verdict #mark-a1 input[type=radio][value="yes"]') page.check('.verdict #mark-a2 input[type=radio][value="no"]') page.locator(".verdict #mark-a1 .mark-submit").click() _wait_said(page, "Not saved") page.locator(".verdict .mark-add textarea").fill("a draft that 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_said(page, "Could not save in place") text = _said(page)["text"] page.close() assert "Not saved: a2" in text and text.index("Not saved: a2") < text.index("Could not save in place"), text