feat(blur): reveal all, and the booth blur control (r2b merge 1: D2 + D2b)

The operator ruled blur A, and made it urgent: "per booth blurring is now
important since we are showing up to 4 images."

- Reveal all: one control per booth, in the booth header and the review's
  top bar, outside every data-region. It is in the markup only when
  something is blurred, always `hidden` until the script shows it.
  - The state is sessionStorage per booth, per tab, and nothing reaches
    the server. It is carried as one `reveal-all` class on <html>, applied
    before first paint from the page's own data-booth, so booth A's reveal
    cannot follow you into booth B and the index is never revealed.
  - Per-item reveal buttons stand down by stylesheet, and an item's own
    reveal is never touched, so "blur again" restores each item as it was.
  - A storage write that throws still applies the click.
- The booth blur control: a plain form to booth-dev's POST /blurbooth, so
  it works with scripts off. Its label follows is_booth_blurred; from the
  review it carries `back` and lands on the same item. A fogged booth's
  Desk row says "◉ blurred".
- Found by rendering it: under a fogged booth every item reported
  `blurred`, so an item blurred only by the booth offered an un-blur that
  visibly did nothing. The gallery now carries `blurred_self`, and such an
  item shows "◉ booth", a label rather than a control.

Contract docs/contracts/r2b_desk_reveal_theme.contract.md (heid contract
panel 4/4, folded). tests/mutations/r2b.toml: 14/14 proved. 765 passed.
This commit is contained in:
vh
2026-09-23 17:52:33 -07:00
parent 091f4b5f2d
commit 5ded5ffe55
11 changed files with 759 additions and 6 deletions
+134
View File
@@ -550,3 +550,137 @@ def test_the_wipe_dialog_shows_what_is_being_wiped_and_never_fails_open(browser,
shown = said[0].split("\n\n")[0]
assert not any(c in shown for c in "‮\n"), repr(shown)
assert "safe�gnp.xe�line2" in shown, repr(shown)
_FILTER = "sel => { const e = document.querySelector(sel); return e ? getComputedStyle(e).filter : 'MISSING'; }"
def _blurred_set(root: pathlib.Path, name: str = "g", blur=("a.png",), flag=("a.png",)) -> pathlib.Path:
from booth.app import set_blurred
from booth.marks import set_flag
b = root / name
b.mkdir()
for rel in ("a.png", "b.png", "c.png"):
(b / rel).write_bytes(PNG)
for rel in blur:
set_blurred(b, rel, True)
for rel in flag:
set_flag(b, rel, True)
return b
def _settle(page) -> None:
page.wait_for_timeout(400) # the filter transition (--dur-2)
def test_reveal_all_reveals_every_blurred_surface_and_survives_the_next_page(browser, live):
"""r2b D2 (blur ruling A): one click lifts the blur on the tile, the tray,
the review stage and the filmstrip, and it holds on the next page of the
same tab. A fresh tab is blurred again — per tab, never persisted."""
base, root = live
_blurred_set(root)
ctx = browser.new_context(viewport={"width": 1400, "height": 900})
page = ctx.new_page()
page.goto(f"{base}/b/g/", wait_until="networkidle")
tile, tray = 'figure.item[data-item="a.png"] img', ".tray-item.is-blurred img"
before = [page.evaluate(_FILTER, tile), page.evaluate(_FILTER, tray)]
page.locator("[data-reveal-all]").click()
_settle(page)
after = [page.evaluate(_FILTER, tile), page.evaluate(_FILTER, tray)]
page.goto(f"{base}/b/g/view?f=a.png", wait_until="networkidle")
_settle(page)
review = [page.evaluate(_FILTER, "#vimg"), page.evaluate(_FILTER, ".film-f.is-blurred img"),
page.locator("[data-reveal-all]").inner_text()]
fresh = ctx.new_page() # a new tab: sessionStorage is per tab
fresh.goto(f"{base}/b/g/view?f=a.png", wait_until="networkidle")
_settle(fresh)
again = fresh.evaluate(_FILTER, "#vimg")
ctx.close()
assert all("blur" in f for f in before), before
assert after == ["none", "none"], after
assert review[:2] == ["none", "none"] and "blur again" in review[2], review
assert "blur" in again, again
def test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk(browser, live):
"""r2b D2 + INV-4: the reveal is scoped to the booth you are in. Booth A's
cannot follow you into booth B, and nothing on the index is revealed."""
base, root = live
_blurred_set(root, "ga")
_blurred_set(root, "gb")
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/b/ga/", wait_until="networkidle")
page.locator("[data-reveal-all]").click()
page.goto(f"{base}/b/gb/", wait_until="networkidle")
_settle(page)
b_tile = page.evaluate(_FILTER, 'figure.item[data-item="a.png"] img')
page.goto(f"{base}/", wait_until="networkidle")
desk = page.evaluate("""() => [...document.querySelectorAll('.desk-strip img.blurred-thumb')]
.map(i => getComputedStyle(i).filter)""")
page.close()
assert "blur" in b_tile, b_tile
assert desk and all("blur" in f for f in desk), desk
def test_reveal_all_survives_an_in_place_save(browser, live):
"""r2b D2: after an in-place save the blur is still lifted — the swapped-in
tile included — the control still says "blur again" and still works, and
the per-tile reveal buttons are still stood down."""
base, root = live
_blurred_set(root, blur=("a.png", "b.png"), flag=())
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/b/g/", wait_until="networkidle")
page.evaluate("window.__same_page = 1")
page.locator("[data-reveal-all]").click()
page.locator('figure.item[data-item="b.png"] .flagtoggle button').click()
page.wait_for_selector('figure.item.is-flagged[data-item="b.png"]', timeout=10000)
_settle(page)
got = {"same": page.evaluate("window.__same_page === 1"),
"b": page.evaluate(_FILTER, 'figure.item[data-item="b.png"] img'),
"label": page.locator("[data-reveal-all]").inner_text(),
"tile_btn": page.evaluate("""() => getComputedStyle(
document.querySelector('figure.item[data-item="b.png"] .reveal')).display""")}
page.locator("[data-reveal-all]").click()
_settle(page)
got["back"] = page.evaluate(_FILTER, 'figure.item[data-item="b.png"] img')
page.close()
assert got["same"] and got["b"] == "none" and "blur again" in got["label"], got
assert got["tile_btn"] == "none" and "blur" in got["back"], got
def test_blur_again_restores_each_items_own_reveal(browser, live):
"""r2b D2: Reveal all never touches an item's own reveal, so "blur again"
returns each item exactly as it was — one revealed on its own stays so."""
base, root = live
_blurred_set(root, blur=("a.png", "b.png"), flag=())
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/b/g/", wait_until="networkidle")
page.locator('figure.item[data-item="a.png"] .reveal').click()
page.locator("[data-reveal-all]").click()
page.locator("[data-reveal-all]").click()
_settle(page)
got = [page.evaluate(_FILTER, f'figure.item[data-item="{r}"] img') for r in ("a.png", "b.png")]
page.close()
assert got[0] == "none" and "blur" in got[1], got
def test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click(browser, live):
"""r2b D2: without JS the control is in the markup but never shown. With
sessionStorage throwing on write (a private window), the click still
applies to the page — only the memory is lost."""
base, root = live
_blurred_set(root)
ctx = browser.new_context(java_script_enabled=False)
page = ctx.new_page()
page.goto(f"{base}/b/g/", wait_until="networkidle")
nojs = page.locator("[data-reveal-all]").is_visible()
ctx.close()
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.add_init_script("Storage.prototype.setItem = function () { throw new Error('quota'); };")
page.goto(f"{base}/b/g/", wait_until="networkidle")
page.locator("[data-reveal-all]").click()
_settle(page)
lifted = page.evaluate(_FILTER, 'figure.item[data-item="a.png"] img')
page.close()
assert not nojs
assert lifted == "none", lifted