# R2c — the review stage: every falsifier the contract claims # (docs/contracts/r2c_review_stage.contract.md), and the change each forbids. unit = "the review stage: fit / 1:1, the arrows at the picture, drag-pan" [[mutation]] label = "S1 Fit never enlarges (the old max-width/max-height cap)" 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:auto;height:auto;max-width:100%;max-height:100%;object-fit:contain;''' [[mutation]] label = "S1 Fit crops (cover, not contain)" 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:cover;''' [[mutation]] label = "S2 the toggle stays hidden (the per-picture hide is back)" file = "booth/templates/view.html" test = "tests/test_flow_browser.py::test_the_toggle_shows_for_every_picture_and_never_without_js" old = ''' toggle.hidden = false;''' new = ''' toggle.hidden = img.naturalWidth <= stage.clientWidth;''' [[mutation]] label = "S2 the toggle shows without JS (display beats [hidden])" file = "booth/templates/base.html" test = "tests/test_flow_browser.py::test_the_toggle_shows_for_every_picture_and_never_without_js" old = ''' .vtoggle[hidden]{display:none}''' new = ''' ''' [[mutation]] label = "S2 1:1 applied late (after the stage exists: a Fit flash)" file = "booth/templates/base.html" test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes" old = ''' if (localStorage.getItem('booth.fit') === 'one') d.classList.add('stage-one');''' new = ''' if (localStorage.getItem('booth.fit') === 'one') document.addEventListener('DOMContentLoaded', function () { d.classList.add('stage-one'); });''' [[mutation]] label = "S2 a stray stored value is taken as 1:1" file = "booth/templates/base.html" test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes" old = ''' if (localStorage.getItem('booth.fit') === 'one') d.classList.add('stage-one');''' new = ''' if (localStorage.getItem('booth.fit')) d.classList.add('stage-one');''' [[mutation]] label = "S2 a storage write that throws swallows the click" file = "booth/templates/view.html" test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes" old = ''' var setMode = function (one) { d.classList.toggle('stage-one', one);''' new = ''' var setMode = function (one) { localStorage.setItem('booth.fit', 'x'); d.classList.toggle('stage-one', one);''' [[mutation]] label = "S3 the arrows stay at the stage edges (never placed)" 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) {''' new = ''' var p = null; if (!p) {''' [[mutation]] label = "S3 the arrows track the file's natural width, not the drawn picture" 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 k = Math.min(b.width / img.naturalWidth, b.height / img.naturalHeight), w = img.naturalWidth * k;''' new = ''' var k = 1, w = img.naturalWidth * k;''' [[mutation]] label = "S3 an arrow is not clamped inside 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 = ''' x = Math.max(lo, Math.min(hi, x));''' new = ''' ''' # NEITHER path: the first draft of this row disabled only the ResizeObserver and # fell through to the window listener, so it stayed green — vacuous. [[mutation]] label = "S3 the arrows do not follow a resize" 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 = ''' if (window.ResizeObserver) new ResizeObserver(settle).observe(stage); else window.addEventListener('resize', settle);''' new = ''' ''' [[mutation]] label = "S4 the pan runs backwards (the picture flees the pointer)" 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 = ''' stage.scrollLeft = drag.l - dx;''' new = ''' stage.scrollLeft = drag.l + dx;''' [[mutation]] 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 (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */''' new = ''' ''' [[mutation]] label = "S4 the picture is draggable again" 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 = '''alt="{{ file }}" draggable="false">''' new = '''alt="{{ file }}">''' [[mutation]] label = "S4 no grab cursor on a pannable 1:1 picture" file = "booth/templates/base.html" test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away" old = ''' .stage-one .vstage.can-pan{cursor:grab;user-select:none}''' new = ''' .stage-one .vstage.can-pan{user-select:none}''' [[mutation]] label = "S4 the stage reveal back inside the scrolled content (a pan carries it off)" file = "booth/templates/base.html" test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away" 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;'''