diff --git a/BUGS-BY-HAND.md b/BUGS-BY-HAND.md index 013ae8e..0a3440d 100644 --- a/BUGS-BY-HAND.md +++ b/BUGS-BY-HAND.md @@ -58,3 +58,4 @@ toe (vaak in `tests/e2e/test_every_page.py`) en zet hier een regel bij. | 2026-11-06 | Nieuwe zin onder de uitgaveregels (KOR uit) bleef Nederlands in het Engels: een "%" in een templatetekst staat in de catalogus als "%%" (M21 deel 2, deze PR) | onvertaalde tekst op een scherm dat `test_every_page` niet ziet | `tests/core/test_translations.py`: elke `{% translate %}`-tekst in elke template wordt in het Engels gevonden zoals Django hem opzoekt | | 2026-11-07 | Kwartaaltabel van het jaaroverzicht rechts afgeknipt op een telefoon ("Winst", en zonder KOR "Voorbelasting", onzichtbaar); de factuurlijst 30px op 1024px (M21 deel 3, deze PR) | tabel breder dan zijn kaart wordt afgeknipt | `test_every_page`: geen `.table-wrap` met `overflow: hidden` en een bredere tabel; `.table-wrap` scrolt nu opzij | | 2026-11-07 | De eigenaar zocht de plek voor de API-sleutel van zijn eigen AI-dienst en vond hem niet: die stond onder Instellingen → "Bonnetjes lezen", een naam zonder AI of sleutel, en op AI-assistenten (waar hij keek) maakt Huismus juist zelf een sleutel (deze PR) | instelling vindbaar alleen voor wie weet waarvoor hij dient | de kaart heet "Bonnetjes lezen met AI" en noemt de API-sleutel, AI-assistenten wijst ernaar; `test_the_card_says_it_is_where_your_own_ai_key_goes`, `test_the_page_points_to_where_your_own_ai_key_goes`. Vindbaarheid zelf: geen geautomatiseerde check | +| 2026-11-13 | Desktop-app: "Koppel met Claude" deed niets, en geen enkel formulier meer, tot het venster dichtging. Een htmx-verzoek ging weg van een deel dat zichzelf ververst; een paginawissel verving dat deel voordat het antwoord kwam. htmx meldt het einde alleen aan het element zelf, dus app.js bleef wachten en hield elk POST-formulier tegen (de eigenaar; deze PR) | boekhouding van de pagina hangt aan een htmx-event van een element dat weg kan zijn | `test_browser`: `test_a_request_whose_element_is_gone_does_not_hold_up_the_forms` (geldt voor elk htmx-verzoek: app.js telt nu ook het einde van het verzoek zelf, `loadend`) | diff --git a/huismus/core/static/core/app.js b/huismus/core/static/core/app.js index 7b03ce8..c9ae622 100644 --- a/huismus/core/static/core/app.js +++ b/huismus/core/static/core/app.js @@ -122,11 +122,22 @@ const formMethod = (event) => const sending = new Set(); let waiting = false; let leaving = false; -document.addEventListener("htmx:beforeSend", (event) => sending.add(event.detail.xhr)); -document.addEventListener("htmx:afterRequest", (event) => { - sending.delete(event.detail.xhr); +const ended = (xhr) => { + if (!sending.delete(xhr)) return; if (sending.size === 0) document.dispatchEvent(new Event("huismus:idle")); +}; +// htmx:afterRequest comes first, before htmx sends a request queued behind +// this one. But htmx tells only the element that sent it, and once that +// element is gone (a part that fetches itself, replaced by a page change) the +// document never hears it. So the request's own end (loadend: an answer, an +// error or an abort) counts too; without it the request stayed here, and +// every form waited for it until the window closed (BUGS-BY-HAND.md). +document.addEventListener("htmx:beforeSend", (event) => { + const xhr = event.detail.xhr; + sending.add(xhr); + xhr.addEventListener("loadend", () => ended(xhr)); }); +document.addEventListener("htmx:afterRequest", (event) => ended(event.detail.xhr)); document.addEventListener("htmx:beforeRequest", (event) => { if (leaving) event.preventDefault(); }); diff --git a/tests/e2e/test_browser.py b/tests/e2e/test_browser.py index 8d58975..74930b6 100644 --- a/tests/e2e/test_browser.py +++ b/tests/e2e/test_browser.py @@ -235,6 +235,33 @@ def hold_the_totals(route): assert Document.objects.get().lines.get().description == "Uren" +def test_a_request_whose_element_is_gone_does_not_hold_up_the_forms(app): + # BUGS-BY-HAND.md, 2026-11-13: in the desktop app "Koppel met Claude" + # did nothing. An htmx request had gone out from an element that was gone + # by the time its answer came (a part that fetches itself every few + # seconds, replaced by a boosted page change). htmx tells only that + # element, so the document never heard the request had ended, and every + # form after it waited for it, for as long as the window stayed open. + held = [] + app.route("**/vastgelopen/", lambda route: held.append(route)) + app.goto(app.base + "/instellingen/ai-assistenten/") + with app.expect_request("**/vastgelopen/"): + app.evaluate( + """() => { + const part = document.createElement("div"); + part.setAttribute("hx-get", "/vastgelopen/"); + part.setAttribute("hx-trigger", "load"); + document.body.append(part); + htmx.process(part); + }""" + ) + app.evaluate("() => document.querySelector('[hx-get=\"/vastgelopen/\"]').remove()") + held[0].fulfill(status=200, body="") + + app.get_by_role("button", name="Koppelen").click() + expect(app.locator("#sleutel")).to_be_attached() + + def test_a_form_sent_once_ignores_the_next_click_until_back(app): # A double click made the browser cut off the first request and send the # form again; after "Leegmaken" or "Terugzetten" the second one found the