# 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) return;''' new = ''' var p = null; if (!p) return;''' [[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 (Math.abs(dx) < 4 && Math.abs(dy) < 4) return;''' 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;'''