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;