Files
booth/tests/mutations/r2_submit_all.toml
T
vh 377e652670 fix(inplace,embed): input set back mid-flight, report inputs, ambiguous anchors
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.
2026-09-28 16:52:42 -07:00

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;'''