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:
@@ -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,'''
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user