feat(review): the review stage fills, its arrows sit at the picture, 1:1 pans (r2c)
The operator: "fit and 1:1 modes as well as moving the forward and back arrows closer to the edge of the image ... mouse click and pan for 1:1 mode if it exceeds page width (defeat drag drop of image)". Ruled: "Fit may enlarge." - Fit: the picture's box is the stage's inner box, and object-fit: contain draws it whole at the largest size that fits, up or down, never cropped. It works with or without JS. 1:1 is natural pixels. - The Fit | 1:1 toggle shows for every picture; the per-picture hide is gone. It stays hidden without JS. - The mode persists as `stage-one` on <html>, set by the head script before the stage exists, so a 1:1 reel never paints a stage in Fit. Anything stored but "one" reads as Fit. Storage never raises. - The arrows sit wholly outside the DRAWN picture (near edge 8px), clamped 8px inside the stage. They sit over the picture only when it spans the stage, and never over the rail. They are re-placed on load, resize, mode switch and 1:1 scroll, and keep their CSS spot until the drawn box is known. - 1:1 drag-to-pan when the picture overflows either axis: the picture follows the pointer, a 4px threshold, pointer capture, grab/grabbing. The picture is draggable=false. The stage's reveal button moves out of the scrolled content to sit over the stage (a pan carried it off), so no control is a pan source. Contract docs/contracts/r2c_review_stage.contract.md (heid contract panel 4/4 folded; it changed the no-flash mechanism). Declared test changes: the Nyx stage-edge arrow test is replaced; the stage class and the toggle's `hidden` are updated. tests/mutations/r2c.toml 16/16. 803 passed.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# 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;'''
|
||||
Reference in New Issue
Block a user