From 9328159ec14e24b3ddfb2763137fc3cf63d24aff Mon Sep 17 00:00:00 2001 From: "B.D. Murphy" <161863146+bdmurphy73@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:20:43 -0700 Subject: [PATCH] Fix re-voice never firing, by driving it from the event that carries the form Reported: Save & Re-voice still gave no response after the previous fix. That fix bound hx-on::after-request to the form. It never fires. htmx reports a different detail.elt for each phase of this request - measured in a real browser: beforeRequest FORM beforeSwap TR afterRequest TBODY Row swaps get re-parented into a table context, so afterRequest is dispatched on the enclosing TBODY. A listener on the form never sees it, because the event does not bubble downwards. An earlier probe suggested the attribute worked, but that probe used a form outside a table, where elt stays the FORM - it did not reproduce the real page. So the profile was being saved (the PUT fired and swapped) while the re-voice never ran and nothing on screen confirmed anything. That is exactly "no response". - Capture the row index at beforeRequest, where detail.elt is still the form, and match it to the completing request by path at afterRequest so a different response can never trigger a re-voice for the wrong speaker. - Saving now shows a confirmation toast. The button previously looked dead even when the edit did save. - Tests now guard the mechanism, including that hx-on::after-request is not reintroduced on this form. Verified by clicking the button in a real browser on Deal Hunter's Dealer: POST /api/voices/queue with {"speaker_name":"Dealer","row_index":14} and the confirmation shown. Suite: 690 passed, 8 pre-existing. --- tests/test_speaker_edit_submit.py | 41 ++++++++++--- umbraaudio/web/templates/_speaker_row.html | 24 +++++--- umbraaudio/web/templates/speakers.html | 71 ++++++++++++++++++++++ 3 files changed, 119 insertions(+), 17 deletions(-) diff --git a/tests/test_speaker_edit_submit.py b/tests/test_speaker_edit_submit.py index 70dfb7e..0e6e977 100644 --- a/tests/test_speaker_edit_submit.py +++ b/tests/test_speaker_edit_submit.py @@ -77,19 +77,40 @@ def test_every_onsubmit_return_handler_returns_a_boolean() -> None: def test_voice_regen_on_save_is_not_empty() -> None: - """The specific function that was gutted.""" + """The specific handler that was gutted in the voice-queue commit.""" bodies = _function_bodies((TEMPLATES / "speakers.html").read_text()) - assert "queueVoiceRegenOnSave" in bodies, "handler is missing entirely" - body = bodies["queueVoiceRegenOnSave"].strip() - assert body, "queueVoiceRegenOnSave has an empty body" + assert "onSpeakerSaved" in bodies, "handler is missing entirely" + body = bodies["onSpeakerSaved"].strip() + assert body, "onSpeakerSaved has an empty body" assert "enqueueVoice" in body, "it must actually queue a regeneration" + assert "notify" in body, "a save must tell the reviewer it happened" -def test_edit_form_queues_regen_after_a_successful_save() -> None: - """Re-voice must run after the new profile is persisted, not before.""" +def test_edit_form_carries_the_regen_marker() -> None: row = _strip_comments((TEMPLATES / "_speaker_row.html").read_text()) - assert "queueVoiceRegenOnSave" in row - assert "hx-on::after-request" in row, ( - "regeneration should be hooked to after-request so the saved profile " - "is on disk before the voice is rebuilt from it" + assert "data-regen-on-save" in row, ( + "the form must be identifiable so a save can trigger a re-voice" ) + assert "hx-on::after-request" not in row, ( + "htmx dispatches afterRequest on the enclosing TBODY for a row swap, " + "so a listener bound to the form never fires - verified in a real browser" + ) + + +def test_regen_trigger_uses_the_event_that_carries_the_form() -> None: + """detail.elt is the FORM at beforeRequest but the TBODY at afterRequest.""" + js = _strip_comments((TEMPLATES / "speakers.html").read_text()) + assert "htmx:beforeRequest" in js, ( + "the index must be captured at beforeRequest, where detail.elt is still " + "the form; reading it at afterRequest yields a TBODY and finds nothing" + ) + assert "htmx:afterRequest" in js + assert "onSpeakerSaved" in js + + +def test_saving_tells_the_reviewer() -> None: + """A save with no visible response reads as a broken button.""" + bodies = _function_bodies(_strip_comments((TEMPLATES / "speakers.html").read_text())) + body = bodies["onSpeakerSaved"] + assert "enqueueVoice" in body, "it must actually queue a regeneration" + assert "notify" in body, "it must confirm to the reviewer" diff --git a/umbraaudio/web/templates/_speaker_row.html b/umbraaudio/web/templates/_speaker_row.html index 8001ed3..e4168b6 100644 --- a/umbraaudio/web/templates/_speaker_row.html +++ b/umbraaudio/web/templates/_speaker_row.html @@ -171,13 +171,23 @@