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
+139
View File
@@ -0,0 +1,139 @@
# R2b — the Desk row, Reveal all, the theme toggle: every falsifier the
# contract claims (docs/contracts/r2b_desk_reveal_theme.contract.md), and the
# change each forbids. Merge 1 is D2 + D2b (the blur half); merge 2 adds D1 + D3.
unit = "reveal all, the booth blur toggle (merge 1)"
[[mutation]]
label = "D2 reveal all does not lift the tile's blur"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_reveals_every_blurred_surface_and_survives_the_next_page"
old = '''
.reveal-all .item.blurred img,.reveal-all .item.blurred video,'''
new = '''
.reveal-all-OFF .item.blurred img,.reveal-all .item.blurred video,'''
[[mutation]]
label = "D2 no pre-paint re-application: the next page of the reel is blurred again"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_reveals_every_blurred_surface_and_survives_the_next_page"
old = '''
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
new = '''
if (false) d.classList.add('reveal-all');'''
[[mutation]]
label = "D2 the reveal is not scoped to the booth (any reveal in the tab reveals every booth)"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk"
old = '''
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
new = '''
if (b !== null && sessionStorage.length > 0) d.classList.add('reveal-all');'''
[[mutation]]
label = "D2 per-tile reveal buttons do not stand down under reveal all"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_survives_an_in_place_save"
old = '''
.reveal-all .item.blurred .reveal,.reveal-all #vreveal{display:none}'''
new = '''
.reveal-all-OFF .item.blurred .reveal,.reveal-all #vreveal{display:none}'''
[[mutation]]
label = "D2 blur again wipes each item's own reveal"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_blur_again_restores_each_items_own_reveal"
old = '''
var on = d.classList.toggle('reveal-all');
try {'''
new = '''
var on = d.classList.toggle('reveal-all');
document.querySelectorAll('.item.revealed').forEach(function (i) { i.classList.remove('revealed'); });
try {'''
[[mutation]]
label = "D2 a storage write that throws swallows the click"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
old = '''
var on = d.classList.toggle('reveal-all');
try {'''
new = '''
sessionStorage.setItem(key, '1');
var on = d.classList.toggle('reveal-all');
try {'''
[[mutation]]
label = "D2 the control shows without JS (server markup not hidden)"
file = "booth/templates/booth.html"
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
old = '''
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden>'''
new = '''
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all>'''
[[mutation]]
label = "D2 the control is markup even when nothing is blurred"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_reveal_all_is_in_the_markup_only_when_something_is_blurred_and_always_hidden"
old = '''
{% if all_items | selectattr('blurred') | list %}<button'''
new = '''
{% if true %}<button'''
[[mutation]]
label = "D2 the review page does not carry data-booth"
file = "booth/templates/view.html"
test = "tests/test_flow.py::test_reveal_all_is_in_the_markup_only_when_something_is_blurred_and_always_hidden"
old = '''
{% block html_attrs %} data-booth="{{ name }}"{% endblock %}'''
new = '''
{% block html_attrs %}{% endblock %}'''
[[mutation]]
label = "D2b the header control's label does not follow the server's fog state"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
old = '''
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
<button title="{{ 'un-blur the whole booth'''
new = '''
<input type="hidden" name="on" value="1">
<button title="{{ 'un-blur the whole booth'''
[[mutation]]
label = "D2b the review's control drops `back` (fogging ejects you from the review)"
file = "booth/templates/view.html"
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
old = '''
<input type="hidden" name="back" value="{{ file }}">'''
new = '''
'''
[[mutation]]
label = "D2b the Desk row does not say a booth is fogged"
file = "booth/templates/index.html"
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
old = '''
{% if b.booth_blurred %}<span class="badge badge-blur"'''
new = '''
{% if false %}<span class="badge badge-blur"'''
[[mutation]]
label = "D2b an item blurred only by the booth offers a per-item un-blur that does nothing"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_under_a_fogged_booth_each_items_blur_control_tells_the_truth"
old = '''
{% if it.blurred and not it.blurred_self %}'''
new = '''
{% if false %}'''
[[mutation]]
label = "D2b the per-item control reads the composed blur, not the item's own"
file = "booth/app.py"
test = "tests/test_flow.py::test_under_a_fogged_booth_each_items_blur_control_tells_the_truth"
old = '''
"blurred_self": it.rel in own_blur,'''
new = '''
"blurred_self": it.blurred,'''
+74
View File
@@ -844,3 +844,77 @@ def test_the_desk_never_makes_a_non_web_url_clickable(tmp_path):
benches = re.search(r'data-panel="benches".*?</section>', body, re.S).group(0)
assert 'href="javascript:' not in benches
assert 'href="http://h:1/"' in benches and "evil bench" in benches
def _regions(body: str) -> list[str]:
"""Every data-region element's full markup."""
return [_region(body, rid) for rid in dict.fromkeys(re.findall(r'data-region="([^"]+)"', body))]
def test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review(tmp_path):
"""r2b D2b: the operator's control for booth-dev's whole-booth marker. A
plain form — scripts off, it still works — whose label says what IS
(read from the server), in the booth header and the review's top bar; the
review's carries `back` and lands on the same item. The Desk row says
`blurred` so a fogged strip says why."""
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG})
c = _client(tmp_path)
page = c.get("/b/g/").text
form = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', page, re.S).group(0)
assert 'name="on" value="1"' in form and "blur booth" in form
assert all(form not in r for r in _regions(page)), "a swap must never replace it"
assert "badge-blur" not in re.search(r'data-booth="g".*?</article>', c.get("/").text, re.S).group(0)
r = c.post("/b/g/blurbooth", data={"on": "1"}, follow_redirects=False)
assert r.status_code == 303 and (b / ".blurbooth").exists()
page = c.get("/b/g/").text
form = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', page, re.S).group(0)
assert 'name="on" value="0"' in form and "booth blurred" in form
row = re.search(r'data-booth="g".*?</article>', c.get("/").text, re.S).group(0)
assert re.search(r'<span class="badge[^"]*badge-blur[^"]*"[^>]*>[^<]*blurred</span>', row)
view = c.get("/b/g/view?f=b.png").text
vbar = re.search(r'<div class="vbar">.*?</div>\s*\n', view, re.S).group(0)
vform = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', vbar, re.S).group(0)
assert 'name="back" value="b.png"' in vform and 'name="on" value="0"' in vform
r = c.post("/b/g/blurbooth", data={"on": "0", "back": "b.png"}, follow_redirects=False)
assert r.headers["location"] == "/b/g/view?f=b.png" and not (b / ".blurbooth").exists()
def test_reveal_all_is_in_the_markup_only_when_something_is_blurred_and_always_hidden(tmp_path):
"""r2b D2: the control is server markup only when the booth has a blurred
item, always with `hidden` (the script unhides it; with JS off it never
shows), and always OUTSIDE every data-region so no in-place swap replaces
it. Every page rendered for one booth carries `data-booth` on <html>; the
index carries none, so nothing there can be revealed."""
from booth.app import set_blurred
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG, "n.md": b"# n"})
c = _client(tmp_path)
assert not re.search(r"<button[^>]*data-reveal-all", c.get("/b/g/").text)
set_blurred(b, "a.png", True)
for url in ("/b/g/", "/b/g/view?f=a.png", "/b/g/view?f=b.png"):
page = c.get(url).text
ctl = re.findall(r'<button[^>]*data-reveal-all[^>]*>', page)
assert len(ctl) == 1 and " hidden" in ctl[0], (url, ctl)
assert all(not re.search(r"<button[^>]*data-reveal-all", r) for r in _regions(page)), url
for url in ("/b/g/", "/b/g/view?f=a.png", "/b/g/view?f=n.md", "/b/g/marks"):
assert re.search(r'<html lang="en" data-booth="g">', c.get(url).text), url
assert re.search(r'<html lang="en">', c.get("/").text)
def test_under_a_fogged_booth_each_items_blur_control_tells_the_truth(tmp_path):
"""r2b D2b, found rendering it: in a fogged booth every item reports
`blurred`, so an item blurred ONLY by the booth offered "◉ blurred" and an
un-blur that visibly did nothing (the booth still fogged it). Its control
now says it is blurred with the booth and offers no per-item action; an item
blurred in its own right keeps its own un-blur."""
from booth.app import set_blurred
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG})
set_blurred(b, "a.png", True)
(b / ".blurbooth").write_bytes(b"")
page = _client(tmp_path).get("/b/g/").text
fig = lambda rel: re.search(r'<figure[^>]*data-item="%s".*?</figure>' % re.escape(rel), page, re.S).group(0)
own, booth = fig("a.png"), fig("b.png")
assert re.search(r'action="/b/g/blur".*?name="on" value="0".*?◉ blurred', own, re.S)
assert 'action="/b/g/blur"' not in booth
assert "◉ booth" in booth
+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