fix(as-S5a): every control named, one h1 and a skip link, rings and hit areas

The markup and CSS half of the anti-slop interaction work. The in-place
client is untouched (that is S5b).
- Glyph-only controls carry a name: the withdraw ×s, downloads, open full
  page, the viewers' ✕, the board's pin, copy and remove, the bench's
  remove, the 1:1 toggle ("1:1, natural pixels"). Film-strip and tray
  frames carry the file's name as sr-only text instead of reading "01".
  A Desk row's wipe names its booth.
- Fields are named by aria-label, not by their placeholder.
- The inline ask's options are a radiogroup labelled by the prompt; a
  single-question fieldset gets an sr-only legend; a titled ask's title
  takes bk-ask-<id>-title (it duplicated the question's id).
- One h1 per page (sr-only on the Desk, review and compare), a skip link
  to <main id="main">, theme-color for light and dark.
- The review tape is one picture (role=img); its segments leave the tab
  order (the film strip holds the same links, named).
- Wipe now uses the Desk's delegated prompt, moved to base.html: it names
  the booth and asks the kept-booth question for a kept booth.
- Embed focus rings of its own; rings drawn inside clipping containers;
  the withdraw × at least 24px, 44px under a coarse pointer;
  touch-action:manipulation; strips contain their overscroll; a long
  slug wraps on a phone.
- A truncated why carries its full text in title; a countdown of 48h or
  more reads in days.
Two r2_flow.toml rows for the confirm helper now name base.html, where
the helper moved (anchors unchanged; the gate found them drifted).

Contract: as_antislop S5a. Falsifiers: antislop.toml 86/86 proved (S1-S6, S5a);
all 12 tables 366/366 proved on this tree.
This commit is contained in:
vh
2026-09-28 13:30:31 -07:00
parent ec807fe41b
commit d4f64fd7ec
16 changed files with 675 additions and 82 deletions
+59
View File
@@ -172,3 +172,62 @@ def test_the_hazard_stripe_is_a_pseudo_element(browser, live):
assert got["own"] == "none", f"the button paints its own stripe: {got['own'][:80]}"
assert "repeating-linear-gradient" in got["pre"] and got["h"] == "3px" and got["content"] != "none", got
page.close()
# ---- S5a: hit areas, and Wipe now asks by name -------------------------------------
def test_withdraw_buttons_are_big_enough_to_hit(browser, live):
from booth.marks import write_note
base, root = live
g = root / "g"
g.mkdir()
(g / "a.png").write_bytes(_png(64, 48))
write_note(g, "a.png", "soft")
for ctx_args, floor in (({"viewport": {"width": 1280, "height": 800}}, 24),
({"viewport": PHONE, "is_mobile": True, "has_touch": True}, 44)):
ctx = browser.new_context(**ctx_args)
page = ctx.new_page()
page.goto(f"{base}/b/g/marks", wait_until="load") # visible at both widths
box = page.locator(".mark-x").first.evaluate("e => e.getBoundingClientRect().height")
assert box >= floor, (ctx_args, box)
ctx.close()
def test_wipe_now_asks_by_name_in_the_browser(browser, live):
base, root = live
for name, kept in (("alpha", False), ("beta", True)):
(root / name).mkdir()
(root / name / "x.txt").write_text("x")
if kept:
(root / name / ".forever").write_text("")
page = browser.new_page(viewport={"width": 1280, "height": 800})
page.goto(f"{base}/b/{name}/", wait_until="load")
said = []
page.on("dialog", lambda d: (said.append(d.message), d.dismiss()))
page.locator(".wipe-lg button").click()
page.wait_for_timeout(300)
assert said and f"“{name}”" in said[0], said
assert ("KEPT" in said[0]) == kept, said[0]
assert (root / name).exists(), "dismissing the prompt must not wipe"
page.close()
def test_touch_and_scroll_behaviour(browser, live):
"""No double-tap zoom delay on a control; the film strip does not hand its
scroll to the page at its end; a long booth slug wraps on a phone instead of
running off the edge (measured on COMPUTED style, not the stylesheet)."""
base, root = live
g = root / "a-very-long-booth-name-that-runs-off-a-phone-screen-2026-09-28"
g.mkdir()
(g / "a.png").write_bytes(_png(64, 48))
(g / "b.png").write_bytes(_png(64, 48))
(g / ".booth.json").write_text('{"handle": "design-dev", "title": "Review"}') # the slug shows beside a title
page = browser.new_page(viewport=PHONE)
page.goto(f"{base}/b/{g.name}/view?f=a.png", wait_until="load")
assert page.locator(".vbar a.vx").evaluate("e => getComputedStyle(e).touchAction") == "manipulation"
assert page.locator(".film").evaluate("e => getComputedStyle(e).overscrollBehaviorX") == "contain"
page.goto(f"{base}/b/{g.name}/", wait_until="load")
slug = page.locator(".h1-slug").evaluate(
"e => ({ws: getComputedStyle(e).whiteSpace, r: e.getBoundingClientRect().right, vw: innerWidth})")
assert slug["ws"] == "normal" and slug["r"] <= slug["vw"], slug
page.close()