fix(review): the heid code-review and bug-hunt panels on r2c, folded (both 4/4 with retries)

- 1:1 start-aligns. The centred flex item overflowed both sides and the
  start was unreachable; measured, a 3000px picture hid its leftmost
  980px. Auto margins still centre a small picture.
- Drag lifecycle: a move with no button ends the drag, so a press
  released outside the stage never pans on a later hover. Capture is now
  load-bearing in a test. The threshold is 4px of total movement.
- A press on the stage's own scrollbar is never a pan. The arrows clamp
  to the stage's client box, so they are never under a classic
  scrollbar. The test runs a browser without --hide-scrollbars and
  asserts the gutter exists.
- Stacked, the arrows' CSS spot is the stage's centre (30vh), set in
  view.html because base.html lost to the page's later rule.
- The stage reveal is `hidden` until bound, and keeps Fit's drop shadow
  when revealed. A blurred picture composes blur() drop-shadow().
- The mode follows another tab. A failed or unknown size returns the
  arrows to their CSS spot.
- Tests: object-position, vertical centring, the Fit half of
  aria-pressed, a storage read that throws, a large picture's toggle,
  Fit forgetting 1:1, single-axis pan.
- Declared: the r2b reveal test reads "no blur" (the shadow stays), and
  the r2_flow 360px-offset row is retired.

Mutation tables 137/137 across four. 810 passed.
This commit is contained in:
vh
2026-09-24 00:20:15 -07:00
parent 7151a45ec2
commit 7c879e6038
6 changed files with 546 additions and 46 deletions
+198 -3
View File
@@ -75,10 +75,10 @@ file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_arrows_sit_just_outside_the_picture_and_clamp_to_the_stage"
old = '''
var p = drawn();
if (!p) return;'''
if (!p) {'''
new = '''
var p = null;
if (!p) return;'''
if (!p) {'''
[[mutation]]
label = "S3 the arrows track the file's natural width, not the drawn picture"
@@ -124,7 +124,7 @@ label = "S4 no drag threshold (a jittery click pans)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
old = '''
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return;'''
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
new = '''
'''
@@ -152,3 +152,198 @@ old = '''
.review-body > .reveal{position:absolute;top:14px;left:14px;z-index:5;'''
new = '''
.review-body > .reveal{position:absolute;top:14px;left:14px;z-index:-1;'''
# ---- the heid code-review fold ("VÍGUNDR")
[[mutation]]
label = "1:1 centres a large picture (its start side can never be scrolled to)"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_in_one_to_one_every_pixel_of_a_large_picture_is_reachable"
old = '''
.stage-one .vstage.is-img{overflow:auto;padding:0;justify-content:flex-start;align-items:flex-start}'''
new = '''
.stage-one .vstage.is-img{overflow:auto;padding:0}'''
[[mutation]]
label = "a buttonless hover continues a press released outside the stage"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_pan_holds_past_the_stage_edge_and_never_starts_on_a_hover"
old = '''
if (!(e.buttons & 1)) { endDrag(); return; }'''
new = '''
'''
[[mutation]]
label = "no pointer capture (a pan dies at the stage's edge)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_pan_holds_past_the_stage_edge_and_never_starts_on_a_hover"
old = '''
try { stage.setPointerCapture(drag.id); } catch (x) {}'''
new = '''
'''
[[mutation]]
label = "stacked, the arrows' fallback centres on stage AND rail"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_before_placement_the_arrows_never_sit_over_the_rail_on_a_narrow_screen"
old = '''
@media (max-width:900px){.vnav{top:30vh}}'''
new = '''
'''
[[mutation]]
label = "Fit shifts the picture off-centre (object-position), cropping it"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_fit_fills_the_stage_up_or_down"
old = '''
.vstage.is-img img{width:100%;height:100%;object-fit:contain;'''
new = '''
.vstage.is-img img{width:100%;height:100%;object-fit:contain;object-position:-100px 50%;'''
[[mutation]]
label = "the arrows are not centred on the stage"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_arrows_sit_just_outside_the_picture_and_clamp_to_the_stage"
old = '''
a.style.top = (s.top - o.top + s.height / 2) + 'px';'''
new = '''
a.style.top = (s.top - o.top + s.height / 4) + 'px';'''
[[mutation]]
label = "a booth.fit read that throws raises out of the head script"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
old = '''
try {
if (localStorage.getItem('booth.fit') === 'one') d.classList.add('stage-one');
} catch (e) {}'''
new = '''
if (localStorage.getItem('booth.fit') === 'one') d.classList.add('stage-one');'''
[[mutation]]
label = "the drag threshold is per axis, not total (a 3,3 diagonal pans nothing)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
old = '''
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
new = '''
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return;'''
# ---- the heid bug-hunt fold ("ÞREKJUR"). Accepted, not rowed: no `touch-action`
# (on touch the stage scrolls natively and the pan yields on pointercancel);
# dragstart preventDefault beside draggable=false (defence in depth, one layer
# alone holds).
[[mutation]]
label = "the stage reveal shows with scripts off (and does nothing)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_stage_reveal_never_shows_without_js_and_keeps_the_fit_shadow"
old = '''aria-label="reveal {{ file }}" hidden>'''
new = '''aria-label="reveal {{ file }}">'''
[[mutation]]
label = "a revealed review picture loses Fit's shadow"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_the_stage_reveal_never_shows_without_js_and_keeps_the_fit_shadow"
old = '''
.review .vstage.is-img.is-blurred.revealed img,.reveal-all .review .vstage.is-img.is-blurred img{'''
new = '''
.review .vstage.is-img.is-blurred.revealed-OFF img,.reveal-all .review .vstage.is-img.is-blurred img{'''
[[mutation]]
label = "a stage mode chosen in another tab does not reach this one"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_stage_mode_chosen_in_one_tab_moves_the_others"
old = '''
if (e.key !== 'booth.fit' && e.key !== null) return;'''
new = '''
return;'''
[[mutation]]
label = "the drag threshold drops to 3px"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
old = '''
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
new = '''
if (dx * dx + dy * dy < 9) return; /* under 4px in all: a click */'''
[[mutation]]
label = "a storage write that throws cuts the click short (buttons never update)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
old = '''
try {
if (one) localStorage.setItem('booth.fit', 'one'); else localStorage.removeItem('booth.fit');
} catch (e) {}'''
new = '''
if (one) localStorage.setItem('booth.fit', 'one'); else localStorage.removeItem('booth.fit');'''
[[mutation]]
label = "the Fit button's pressed state is never drawn"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
old = '''
bFit.setAttribute('aria-pressed', one ? 'false' : 'true');'''
new = '''
'''
# ---- groa's retry supplement (code review)
[[mutation]]
label = "S1 Fit loses its drop shadow"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_fit_fills_the_stage_up_or_down"
old = '''
.vstage.is-img img{width:100%;height:100%;object-fit:contain;
filter:drop-shadow(0 10px 24px rgb(0 0 0 / .32))}'''
new = '''
.vstage.is-img img{width:100%;height:100%;object-fit:contain}'''
[[mutation]]
label = "S2 the toggle hides, on load, for a picture larger than the stage"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_toggle_shows_for_every_picture_and_never_without_js"
old = '''
function settle() { place(); pannable(); }'''
new = '''
function settle() { place(); pannable(); if (img) document.getElementById('vtoggle').hidden = img.naturalWidth > stage.clientWidth; }'''
[[mutation]]
label = "S2 choosing Fit stores a word instead of forgetting 1:1"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
old = '''
if (one) localStorage.setItem('booth.fit', 'one'); else localStorage.removeItem('booth.fit');'''
new = '''
if (one) localStorage.setItem('booth.fit', 'one'); else localStorage.setItem('booth.fit', 'fit');'''
[[mutation]]
label = "S4 pan only when BOTH axes overflow"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_picture_that_overflows_one_axis_pans_along_it"
old = '''
(stage.scrollWidth > stage.clientWidth || stage.scrollHeight > stage.clientHeight);'''
new = '''
(stage.scrollWidth > stage.clientWidth && stage.scrollHeight > stage.clientHeight);'''
# ---- groa's retry supplement (bug hunt): classic scrollbars
[[mutation]]
label = "a press on the stage's scrollbar starts a pan"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_classic_scrollbar_is_neither_under_an_arrow_nor_a_pan"
old = '''
if (e.clientX - r.left - stage.clientLeft >= stage.clientWidth ||
e.clientY - r.top - stage.clientTop >= stage.clientHeight) return;'''
new = '''
'''
[[mutation]]
label = "the arrows clamp to the border box (the next one sits under a classic scrollbar)"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_a_classic_scrollbar_is_neither_under_an_arrow_nor_a_pan"
old = '''
var cl = s.left + stage.clientLeft, cr = cl + stage.clientWidth;'''
new = '''
var cl = s.left, cr = s.right;'''