feat(r2): C5 the lightbox, and the in-place client
- On a gallery booth the marks panel moves into a sticky verdict aside beside the set. The aside comes first in the document, so a narrow screen stacks the question above the work; grid areas place it on the right when wide. Nothing in an ordered collection moves. Boards are unchanged. - The flag tray lists flagged items by tile number: the declared change from the panel list's (created, id). The standalone marks page keeps the list. - Inline group headers are divs, never figure.item. - Every mark-dependent element is a data-region: the verdict, each tile, the rail's filter counts. There is also a server-rendered status line. - The in-place script (base.html) POSTs with an explicit JSON Accept, then on 204 swaps every region from a fresh GET. Live media and per-viewer view state are carried across the swap, so there is no layout jolt and no stopped track. It never re-POSTs: on failure it says so and reloads. Tile controls re-bind after a swap, and the grid cursor survives it. - The `n` key opens the tile's closed note disclosure before focusing it. - test_embed_browser's keyboard-flag test expected a navigation, which is the defect R2 removes. It is updated as declared in the contract, and tightened: a window marker must survive, proving no reload. Browser tests: flag in place, with no reload and no scroll jump, and the tile, tray and rail count all updated; and a failed save that reloads without re-POSTing. Two mutations turn them red (no carry, no rail region). 700 passed.
This commit is contained in:
@@ -407,3 +407,72 @@ def test_a_booth_without_images_shows_its_kind_instead(tmp_path):
|
||||
_booth(tmp_path, "songs", {"a.mp3": b"ID3", "b.mp3": b"ID3"})
|
||||
row = re.search(r'data-booth="songs".*?</article>', _client(tmp_path).get("/").text, re.S).group(0)
|
||||
assert "♪ audio" in row and "<img" not in row
|
||||
|
||||
|
||||
# ---- C5: the lightbox ---------------------------------------------------------
|
||||
|
||||
def _region(body: str, rid: str) -> str:
|
||||
m = re.search(r'<(\w+)[^>]*data-region="%s"[^>]*>' % re.escape(rid), body)
|
||||
assert m, f"no region {rid}"
|
||||
tag = m.group(1)
|
||||
# regions in these templates do not nest a same-named tag inside themselves
|
||||
end = body.index(f"</{tag}>", m.end())
|
||||
return body[m.start():end]
|
||||
|
||||
|
||||
def test_the_verdict_sits_beside_the_set_on_a_gallery_booth(tmp_path):
|
||||
"""The tracer for C5: the open question, the flags and the notes live in
|
||||
one aside next to the grid — not in a panel above it that scrolls away."""
|
||||
from booth.marks import declare_pick
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG})
|
||||
declare_pick(b, "q", {"prompt": "Which one?", "options": ["a", "b"]})
|
||||
body = _client(tmp_path).get("/b/g/").text
|
||||
aside = _region(body, "verdict")
|
||||
assert aside.startswith('<aside class="verdict"')
|
||||
assert 'action="/b/g/answer"' in aside and "Which one?" in aside
|
||||
assert body.count('action="/b/g/answer"') == 1, "the pick renders once, in the aside"
|
||||
assert 'class="lightbox"' in body and 'id="grid"' in body
|
||||
|
||||
|
||||
def test_the_flag_tray_lists_flags_by_tile_number_not_by_click_order(tmp_path):
|
||||
"""Flag #3 first, then #1: the tray reads #1, #3. Today's panel list is in
|
||||
click order `(created, id)`; the tray is the SET's order, the declared
|
||||
change. Each entry is the original shown small, blurred if the item is."""
|
||||
from booth.app import set_blurred
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG, "c.png": PNG})
|
||||
set_flag(b, "c.png", True)
|
||||
time.sleep(0.01)
|
||||
set_flag(b, "a.png", True)
|
||||
set_blurred(b, "a.png", True)
|
||||
aside = _region(_client(tmp_path).get("/b/g/").text, "verdict")
|
||||
tray = re.findall(r'<a class="tray-item( is-blurred)?" href="view\?f=([^"]+)"[^>]*>.*?#(\d+)<', aside, re.S)
|
||||
assert tray == [(" is-blurred", "a.png", "1"), ("", "c.png", "3")]
|
||||
|
||||
|
||||
def test_groups_get_an_inline_header_that_is_not_a_tile(tmp_path):
|
||||
"""When the rail finds grouping informative, each group's first tile is
|
||||
preceded by a header — a div, never a figure.item, so the tile sequence
|
||||
the keyboard and the order check walk is exactly the items."""
|
||||
files = {f"{g}-{n}.png": PNG for g in ("aa", "bb", "cc") for n in (1, 2)}
|
||||
_booth(tmp_path, "g", files)
|
||||
body = _client(tmp_path).get("/b/g/").text
|
||||
grid = body[body.index('id="grid"'):]
|
||||
heads = re.findall(r'<div class="grp-head"[^>]*><span class="grp-key">(\w+)</span> <span class="grp-n">(\d+)</span>', grid)
|
||||
assert heads == [("aa", "2"), ("bb", "2"), ("cc", "2")]
|
||||
assert len(re.findall(r'<figure class="item', grid)) == 6
|
||||
# and no header at all when grouping is not informative
|
||||
_booth(tmp_path, "flat", {f"DSC{n}.jpg": PNG for n in range(4)})
|
||||
assert 'class="grp-head"' not in _client(tmp_path).get("/b/flat/").text
|
||||
|
||||
|
||||
def test_every_mark_dependent_element_is_a_swappable_region(tmp_path):
|
||||
"""The in-place script replaces regions by id; a stale flag count or tile
|
||||
after an in-place flag is the failure this pins."""
|
||||
b = _booth(tmp_path, "g", {"a b.png": PNG, "c.png": PNG})
|
||||
body = _client(tmp_path).get("/b/g/").text
|
||||
for rid in ("verdict", "filters", "item-a%20b.png", "item-c.png", "status"):
|
||||
assert f'data-region="{rid}"' in body, rid
|
||||
# the standing board has no lightbox and therefore no verdict aside
|
||||
_booth(tmp_path, "links", {"links.md": b"- [x](http://x/) <sub>\xc2\xb7 a \xc2\xb7 2026-09-01 10:00</sub>\n"})
|
||||
board = _client(tmp_path).get("/b/links/").text
|
||||
assert 'data-region="verdict"' not in board and 'class="lightbox"' not in board
|
||||
|
||||
Reference in New Issue
Block a user