Fix add-form JS: name→sort-name sync and related-game disable
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 <form> 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 <div> (a <div> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <form> 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 <div>
|
||||
(a <div> 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()
|
||||
|
||||
+3
-1
@@ -9,4 +9,6 @@ const syncData = [
|
||||
},
|
||||
];
|
||||
|
||||
syncSelectInputUntilChanged(syncData, "form");
|
||||
// Scope to the add form (#add-form), not "form": the first <form> on the page
|
||||
// is the navbar logout form, which never contains these fields.
|
||||
syncSelectInputUntilChanged(syncData, "#add-form");
|
||||
|
||||
+6
-1
@@ -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 <div>
|
||||
// (ignores `disabled`), so target the inner search <input> instead.
|
||||
disableElementsWhenTrue("#id_type", "game", [
|
||||
"#id_name",
|
||||
"#id_related_game [data-search-select-search]",
|
||||
]);
|
||||
}
|
||||
|
||||
onSwap("#id_type", (typeSelect) => {
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user