From 5bb8b92c05999ddb6ebbf24da900d0b614a586fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Fri, 19 Jun 2026 18:11:49 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20add-form=20JS:=20name=E2=86=92sort-name?= =?UTF-8?q?=20sync=20and=20related-game=20disable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the add forms, both root-caused via the e2e harness: 1. add_game Name → Sort name never synced. syncSelectInputUntilChanged was scoped to "form", but the first
on every page is the navbar logout form — the add-form fields live in a later form, so the delegated listener never heard their events. Scope to "#add-form" (the add-form wrapper). Also switch the sync from the "change" event to "input" so Sort name mirrors Name live as you type, not only on blur. 2. add_purchase Related game not disabled when Type == Game. disableElementsWhenTrue set `.disabled` on #id_related_game, which is the SearchSelect wrapper
(a
ignores `disabled`). Target the inner [data-search-select-search] input instead, so the widget is actually disabled. Adds two e2e regression tests (live sync; type-game disables the related-game search input and re-enables it for other types). Co-Authored-By: Claude Opus 4.8 --- e2e/test_widgets_e2e.py | 25 +++++++++++++++++++++++++ ts/add_game.ts | 4 +++- ts/add_purchase.ts | 7 ++++++- ts/utils.ts | 7 ++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/e2e/test_widgets_e2e.py b/e2e/test_widgets_e2e.py index ae328e7..19fece8 100644 --- a/e2e/test_widgets_e2e.py +++ b/e2e/test_widgets_e2e.py @@ -147,3 +147,28 @@ def test_add_purchase_related_game_is_flat_game_search( related = page.locator('[data-search-select][data-name="related_game"]') expect(related).to_have_count(1) expect(related).to_have_attribute("data-search-url", "/api/games/search") + + +def test_add_game_syncs_sort_name_from_name(authenticated_page: Page, live_server): + """Typing into Name live-fills Sort name (sync bound to the add form, not + the navbar logout form which is the first on the page).""" + page = authenticated_page + page.goto(f"{live_server.url}{reverse('games:add_game')}") + page.locator("#id_name").click() + page.locator("#id_name").type("Halo") + expect(page.locator("#id_sort_name")).to_have_value("Halo") + + +def test_add_purchase_type_game_disables_related_game_search( + authenticated_page: Page, live_server +): + """When Type is 'game', the related-game SearchSelect is disabled — the + real disable target is the inner search input, not the wrapper
+ (a
ignores the disabled property).""" + page = authenticated_page + page.goto(f"{live_server.url}{reverse('games:add_purchase')}") + search = page.locator('#id_related_game [data-search-select-search]') + page.select_option("#id_type", "game") + expect(search).to_be_disabled() + page.select_option("#id_type", "dlc") + expect(search).to_be_enabled() diff --git a/ts/add_game.ts b/ts/add_game.ts index 343e15f..82e7395 100644 --- a/ts/add_game.ts +++ b/ts/add_game.ts @@ -9,4 +9,6 @@ const syncData = [ }, ]; -syncSelectInputUntilChanged(syncData, "form"); +// Scope to the add form (#add-form), not "form": the first on the page +// is the navbar logout form, which never contains these fields. +syncSelectInputUntilChanged(syncData, "#add-form"); diff --git a/ts/add_purchase.ts b/ts/add_purchase.ts index ff8cbc1..7176b31 100644 --- a/ts/add_purchase.ts +++ b/ts/add_purchase.ts @@ -53,7 +53,12 @@ onSwap("#id_separate_prices", (checkbox) => { }); function setupElementHandlers(): void { - disableElementsWhenTrue("#id_type", "game", ["#id_name", "#id_related_game"]); + // related_game is a SearchSelect: its #id_related_game wrapper is a
+ // (ignores `disabled`), so target the inner search instead. + disableElementsWhenTrue("#id_type", "game", [ + "#id_name", + "#id_related_game [data-search-select-search]", + ]); } onSwap("#id_type", (typeSelect) => { diff --git a/ts/utils.ts b/ts/utils.ts index b427a80..75c72ae 100644 --- a/ts/utils.ts +++ b/ts/utils.ts @@ -52,11 +52,12 @@ function syncSelectInputUntilChanged(syncData: Array<{ source: string; target: s console.error(`The parent selector "${parentSelector}" is not valid.`); return; } - // Set up a single change event listener on the document for handling all source changes - parentElement.addEventListener("change", function (event) { + // One delegated "input" listener handles every source. "input" (not "change") + // makes the mirror live as the user types, instead of only on blur. + parentElement.addEventListener("input", function (event) { // Loop through each sync configuration item syncData.forEach((syncItem: { source: string; target: string; source_value: string; target_value: string }) => { - // Check if the change event target matches the source selector + // Check if the event target matches the source selector if ((event.target as HTMLElement).matches(syncItem.source)) { if (!event.target) return; const sourceElement = event.target;