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.
This commit is contained in:
@@ -126,7 +126,7 @@ new = ''''''
|
||||
label = 'C3 client: an unsent radio is not carried across a swap'
|
||||
file = "booth/templates/base.html"
|
||||
test = "tests/test_flow_browser.py::test_an_unsaved_choice_survives_a_save_elsewhere_and_a_double_click_writes_once"
|
||||
old = ''' if (el.checked !== el.defaultChecked) t.checked = el.checked;'''
|
||||
old = ''' if (el.checked !== on) t.checked = el.checked;'''
|
||||
new = ''''''
|
||||
|
||||
[[mutation]]
|
||||
|
||||
@@ -96,15 +96,24 @@ 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 = '''
|
||||
if (recs[i].key === k && recs[i].snap === serial(f)) return true;'''
|
||||
return snap !== null && snap === serial(f);'''
|
||||
new = '''
|
||||
if (recs[i].key === k) return true;'''
|
||||
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 && recs[i].snap === serial(f)) return true;'''
|
||||
if (recs[i].key === k) return recs[i].snap;'''
|
||||
new = '''
|
||||
if (false) return true;'''
|
||||
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;'''
|
||||
|
||||
@@ -384,8 +384,10 @@ label = "D3 a stored theme is not applied at load (a reload forgets it)"
|
||||
file = "booth/templates/base.html"
|
||||
test = "tests/test_flow_browser.py::test_the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live"
|
||||
old = '''
|
||||
var t = localStorage.getItem('booth.theme');
|
||||
if (t === 'light' || t === 'dark') d.setAttribute('data-theme', t);'''
|
||||
new = '''
|
||||
var t = localStorage.getItem('booth.theme');
|
||||
if (false) d.setAttribute('data-theme', t);'''
|
||||
|
||||
[[mutation]]
|
||||
@@ -402,8 +404,10 @@ label = "D3 forced light is not in the sheet"
|
||||
file = "booth/templates/_svos_tokens.css"
|
||||
test = "tests/test_flow_browser.py::test_the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live"
|
||||
old = '''
|
||||
/* ... or the viewer forced light. Same declarations as above, by construction. */
|
||||
:root[data-theme="light"] {'''
|
||||
new = '''
|
||||
/* ... or the viewer forced light. Same declarations as above, by construction. */
|
||||
:root[data-theme="light-OFF"] {'''
|
||||
|
||||
[[mutation]]
|
||||
|
||||
@@ -67,7 +67,7 @@ label = "a refusal reloads the page and clears what was entered"
|
||||
file = "booth/static/embed.js"
|
||||
test = "tests/test_embed_browser.py::test_a_refused_ask_costs_only_itself_and_clears_nothing"
|
||||
old = '''
|
||||
if (!failed.length && !changed) { location.reload(); return; }'''
|
||||
if (!failed.length && !changed && !host) { location.reload(); return; }'''
|
||||
new = '''
|
||||
location.reload(); return;'''
|
||||
|
||||
@@ -76,9 +76,9 @@ label = "only dirtiness blocks the reload (a refused form set back to its first
|
||||
file = "booth/static/embed.js"
|
||||
test = "tests/test_embed_browser.py::test_a_refusal_blocks_the_reload_even_when_nothing_reads_dirty"
|
||||
old = '''
|
||||
if (!failed.length && !changed) { location.reload(); return; }'''
|
||||
if (!failed.length && !changed && !host) { location.reload(); return; }'''
|
||||
new = '''
|
||||
if (!changed) { location.reload(); return; }'''
|
||||
if (!changed && !host) { location.reload(); return; }'''
|
||||
|
||||
[[mutation]]
|
||||
label = "a refusal is never said"
|
||||
@@ -139,3 +139,12 @@ test = "tests/test_embed_browser.py::test_the_empty_status_line_stays_hidden_und
|
||||
old = '''
|
||||
".bk-ask-status[hidden]{display:none}",'''
|
||||
new = ''''''
|
||||
|
||||
[[mutation]]
|
||||
label = "the reload ignores the report's own inputs (text typed into the host page is lost)"
|
||||
file = "booth/static/embed.js"
|
||||
test = "tests/test_embed_browser.py::test_a_clean_batch_does_not_reload_over_text_typed_into_the_report"
|
||||
old = '''
|
||||
if (!failed.length && !changed && !host) { location.reload(); return; }'''
|
||||
new = '''
|
||||
if (!failed.length && !changed) { location.reload(); return; }'''
|
||||
|
||||
@@ -1031,3 +1031,30 @@ def test_a_refusal_blocks_the_reload_even_when_nothing_reads_dirty(browser, live
|
||||
page.close()
|
||||
assert same, "the page reloaded over a refused POST"
|
||||
assert shown, "the refusal was never said"
|
||||
|
||||
|
||||
def test_a_clean_batch_does_not_reload_over_text_typed_into_the_report(browser, live):
|
||||
"""design-dev's report, 2026-09-28: a successful batch reloaded whenever
|
||||
none of OUR forms was dirty — but a report can carry inputs of its own, and
|
||||
the reload took whatever the operator had typed into them. Unsaved input
|
||||
anywhere on the page now holds the reload, and the page says so."""
|
||||
base, data = live
|
||||
b = _asks(data / "b", ("a1", "a2"))
|
||||
html = ("<!doctype html><title>r</title><body>"
|
||||
'<textarea id="scratch"></textarea>'
|
||||
f"{SEAM}</body>")
|
||||
page = _open(browser, base, "b", html, b)
|
||||
page.wait_for_selector("#bk-ask-a2-submit")
|
||||
page.evaluate("window.__same = 1")
|
||||
page.fill("#scratch", "my own working")
|
||||
_choice(page, "a1", "yes")
|
||||
_choice(page, "a2", "no")
|
||||
_press(page, "a1")
|
||||
page.wait_for_selector("#bk-ask-a1-submit .bk-ask-status:not([hidden])", timeout=10000)
|
||||
said = page.locator("#bk-ask-a1-submit .bk-ask-status").inner_text()
|
||||
same = page.evaluate("window.__same === 1")
|
||||
kept = page.input_value("#scratch")
|
||||
page.close()
|
||||
assert same and kept == "my own working", (same, kept)
|
||||
assert "reload" in said.lower(), said
|
||||
assert {k: v["choice"] for k, v in _answers(b).items()} == {"a1": "yes", "a2": "no"}
|
||||
|
||||
@@ -1819,3 +1819,33 @@ def test_a_batch_whose_refresh_fails_keeps_the_page(browser, live):
|
||||
assert _chosen(b) == {"a1": "yes", "a2": "no"}
|
||||
assert same and draft == "not sent", (same, draft)
|
||||
assert "reload" in said.lower(), said
|
||||
|
||||
|
||||
def test_a_sent_pick_set_back_to_its_first_value_mid_flight_is_kept(browser, live):
|
||||
"""design-dev's report, 2026-09-28: carry() measured a changed-after-press
|
||||
form against its OLD DEFAULTS, so an answer set back to the value the page
|
||||
first showed looked untouched — and the swap put the just-saved value
|
||||
back over the operator's last word, silently. A sent form that changed
|
||||
since its press is now measured against what it SENT."""
|
||||
from booth.marks import answer_pick
|
||||
base, root = live
|
||||
b = _set(root, 1)
|
||||
_picks(b, ("a1",))
|
||||
answer_pick(b, "a1", "no")
|
||||
page = browser.new_page()
|
||||
page.add_init_script(_HOLD_POSTS)
|
||||
page.goto(f"{base}/b/g/marks", wait_until="networkidle")
|
||||
page.locator("#mark-a1 summary.mark-change").click()
|
||||
page.check('#mark-a1 input[type=radio][value="yes"]')
|
||||
page.locator("#mark-a1 .mark-submit").click()
|
||||
page.wait_for_timeout(150)
|
||||
page.check('#mark-a1 input[type=radio][value="no"]') # back, mid-flight
|
||||
page.wait_for_function(
|
||||
"document.querySelector('#mark-a1 .mark-answer-choice') && "
|
||||
"document.querySelector('#mark-a1 .mark-answer-choice').textContent.trim() === 'yes'",
|
||||
timeout=10000)
|
||||
page.wait_for_timeout(300)
|
||||
showing = page.is_checked('#mark-a1 input[type=radio][value="no"]')
|
||||
page.close()
|
||||
assert _chosen(b) == {"a1": "yes"}
|
||||
assert showing, "the value set back mid-flight was replaced by the saved one"
|
||||
|
||||
@@ -128,3 +128,17 @@ def test_a_reverted_file_keeps_its_mtime(tmp_path):
|
||||
check({"label": "flip", "file": "mod.py", "test": "test_probe.py::test_f",
|
||||
"old": "return 2", "new": "return 3"}, repo=repo)
|
||||
assert mod.stat().st_mtime_ns == before
|
||||
|
||||
|
||||
def test_an_ambiguous_anchor_is_reported_not_guessed(tmp_path):
|
||||
"""An `old` that matches twice mutates whichever comes FIRST, so the row
|
||||
proves or fails by where the first match happens to fall rather than by
|
||||
the line it names. design-dev found two r2b rows proving that way
|
||||
(2026-09-28). A row must name exactly one place."""
|
||||
repo = _tree(tmp_path, "def f():\n x = 2\n x = 2\n return x\n",
|
||||
"def test_f():\n assert f() == 2\n")
|
||||
proved, note = check(
|
||||
{"label": "twice", "file": "mod.py", "test": "test_probe.py::test_f",
|
||||
"old": " x = 2\n", "new": " x = 3\n"}, repo=repo)
|
||||
assert not proved
|
||||
assert "ambiguous" in note
|
||||
|
||||
Reference in New Issue
Block a user