Four items owed after S5b, reported by design-dev during the anti-slop run:
- carry() measured a sent-then-changed form against its OLD DEFAULTS. An
answer set back mid-flight to the value the page first showed read as
untouched, and the swap put the just-saved value over it. A form sent and
then changed is now measured against its sent snapshot (sentSet.snapOf).
- The embed's clean-batch reload saw only our own forms. A report's own
inputs lost whatever the operator had typed into them. Unsaved text in
any control we don't own now holds the reload, and the page says so.
- Two r2b.toml rows ("D3 a stored theme...", "D3 forced light...") matched
twice, so they proved only by where the first match fell. Both are
re-anchored, and scripts/mutation_check.py now refuses any anchor that
matches more than once. A new tool control covers that.
- The r2_flow contract's C3 steps 2 and 4 now say what S5b superseded. U3
gains the report-input rule.
Mutation rows: u3_submit_all +1, r2_submit_all +1. Four rows were
re-anchored onto the moved lines.
120 lines
5.0 KiB
TOML
120 lines
5.0 KiB
TOML
# R2 C3 step 3a, 2026-09-27: one submit saves every changed pick on a Booth
|
|
# page (the marks page, the lightbox's verdict aside, the review rail). The
|
|
# Booth-page half of the operator's report; the verbatim half is
|
|
# u3_submit_all.toml. Contract: docs/contracts/r2_flow.contract.md, C3 step 3a.
|
|
# The flight-window and refusal rows came from the heid bug-hunt panel on the
|
|
# first cut.
|
|
|
|
unit = "r2 submit all"
|
|
|
|
[[mutation]]
|
|
label = "the other pick forms are ignored (one form, one save, as before)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_one_submit_on_the_marks_page_saves_every_changed_pick"
|
|
old = '''
|
|
var batch = pickBatch(form);'''
|
|
new = '''
|
|
var batch = null;'''
|
|
|
|
[[mutation]]
|
|
label = "the pressed pick is sent even when blank (its 400 reloads the rest away)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_pressing_a_blank_picks_submit_saves_the_others_and_skips_it"
|
|
old = '''
|
|
return others ? picks.filter(function (f) { return dirty(f) && !pending[flightKey(f)]; }) : null;'''
|
|
new = '''
|
|
return others ? picks.filter(function (f) { return (f === form || dirty(f)) && !pending[flightKey(f)]; }) : null;'''
|
|
|
|
[[mutation]]
|
|
label = "a refusal stops the picks after it"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_refused_pick_in_a_batch_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
return post(f, datas[i]).then(function () {'''
|
|
new = '''
|
|
if (refused.length) return;
|
|
return post(f, datas[i]).then(function () {'''
|
|
|
|
[[mutation]]
|
|
label = "a refused batch reloads (the refused pick and every draft are lost)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_refused_pick_in_a_batch_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
var shown = saved.length ? refresh(saved, true) : Promise.resolve(true);'''
|
|
new = '''
|
|
if (refused.length) { fail(); return; }
|
|
var shown = saved.length ? refresh(saved, true) : Promise.resolve(true);'''
|
|
|
|
[[mutation]]
|
|
label = "a refused pick counts as sent, so its input comes back as the server has it"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_refused_pick_in_a_batch_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
}, function (e) { refused.push(askOf(f) + ' (' + e.message + ')'); });'''
|
|
new = '''
|
|
}, function (e) { saved.push({key: flightKey(f), snap: snaps[i]}); refused.push(askOf(f) + ' (' + e.message + ')'); });'''
|
|
|
|
[[mutation]]
|
|
label = "a form in flight stops counting as another dirty form (a clean press 400s mid-save)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_pressing_a_clean_pick_during_a_batch_sends_nothing"
|
|
old = '''
|
|
return f.getAttribute('action') === action && isPick(f);'''
|
|
new = '''
|
|
return f.getAttribute('action') === action && isPick(f) && !pending[flightKey(f)];'''
|
|
|
|
[[mutation]]
|
|
label = "in flight is marked on the node, so a swap's fresh copy can be sent twice"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_pick_in_flight_stays_in_flight_across_another_saves_swap"
|
|
old = '''
|
|
function flightKey(f) { return formKey(f); }'''
|
|
new = '''
|
|
var nth = 0;
|
|
function flightKey(f) { return f.__fk || (f.__fk = 'n' + (++nth)); }'''
|
|
|
|
[[mutation]]
|
|
label = "a new save does not clear the last one's words"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_later_save_clears_a_stale_not_saved_line"
|
|
old = '''
|
|
st.textContent = text;'''
|
|
new = '''
|
|
if (!st.textContent) st.textContent = text;'''
|
|
|
|
[[mutation]]
|
|
label = "a batch whose refresh fails reloads (every unsent draft with it)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_batch_whose_refresh_fails_keeps_the_page"
|
|
old = '''
|
|
var shown = saved.length ? refresh(saved, true) : Promise.resolve(true);'''
|
|
new = '''
|
|
var shown = saved.length ? refresh(saved, true).catch(function (e) { reload(); throw e; }) : Promise.resolve(true);'''
|
|
|
|
[[mutation]]
|
|
label = "a sent form counts as sent even when it changed after the press"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_change_to_a_sent_pick_during_the_flight_is_kept"
|
|
old = '''
|
|
return snap !== null && snap === serial(f);'''
|
|
new = '''
|
|
return snap !== null;'''
|
|
|
|
[[mutation]]
|
|
label = "no form counts as sent (a saved note's text carries back as a draft)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_saved_notes_box_comes_back_empty"
|
|
old = '''
|
|
if (recs[i].key === k) return recs[i].snap;'''
|
|
new = '''
|
|
if (false) return recs[i].snap;'''
|
|
|
|
[[mutation]]
|
|
label = "a sent-then-changed form is measured against its OLD defaults (a value set back mid-flight is lost)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_sent_pick_set_back_to_its_first_value_mid_flight_is_kept"
|
|
old = '''
|
|
var snap = isSent.snapOf ? isSent.snapOf(el.form) : null;'''
|
|
new = '''
|
|
var snap = null;'''
|