= __('Etichetta "ombrello" per spin-off (es. tutto il franchise di Fairy Tail).') ?>
@@ -518,9 +518,9 @@
-
+
= __("Serie superiore nella gerarchia (es. l'universo che contiene cicli e stagioni).") ?>
@@ -537,9 +537,9 @@
-
+
= __("Nome specifico della serie a cui appartiene il libro.") ?>
@@ -554,9 +554,9 @@
-
+
@@ -2232,6 +2232,18 @@ classNames: { containerInner: 'choices__inner' }
const typed = input ? input.value.trim() : '';
const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
const hl = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
+ // A suggestion is highlighted but the user typed something
+ // different: commit the TYPED value instead of letting Choices
+ // pick the highlight (same guard as the publisher field, #74).
+ if (typed && hl) {
+ const nameEl = hl.querySelector('.choices__item-text') || hl.childNodes[0];
+ const highlightedText = (nameEl ? nameEl.textContent : hl.textContent).trim().toLowerCase();
+ if (highlightedText !== typed.toLowerCase()) {
+ event.preventDefault();
+ commitTyped(typed);
+ return;
+ }
+ }
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};
diff --git a/tests/full-test.spec.js b/tests/full-test.spec.js
index d99e5bbe..5af963dd 100644
--- a/tests/full-test.spec.js
+++ b/tests/full-test.spec.js
@@ -687,16 +687,22 @@ test.describe.serial('Phase 3: Manual Book Creation', () => {
await copies.fill('2');
}
- // Series (collana) — now a Choices.js autocomplete (#179) whose submitted
- // value lives in a hidden input set via the exposed setter.
- await page.evaluate((v) => {
- if (window.__seriesAutocomplete && window.__seriesAutocomplete.collana) {
- window.__seriesAutocomplete.collana(v);
- } else {
- const h = document.getElementById('collana');
- if (h) h.value = v;
- }
- }, `TestSeries_${RUN_ID}`);
+ // Series (collana) — now a Choices.js autocomplete (#179). Drive the real
+ // widget per the repo convention (.coderabbit.yaml): fill + waitForTimeout +
+ // click the suggestion. The typed value is always offered as a create-option,
+ // so it works for the brand-new name created here. The dropdown locator is
+ // scoped to this field's wrapper (the page has several Choices widgets).
+ const seriesValue = `TestSeries_${RUN_ID}`;
+ const collanaWrapper = page.locator('.choices', { has: page.locator('#collana_select') });
+ await collanaWrapper.click();
+ const collanaSearch = collanaWrapper.locator('input[type="search"], .choices__input--cloned').first();
+ await collanaSearch.fill(seriesValue);
+ await page.waitForTimeout(350);
+ await collanaWrapper
+ .locator('.choices__list--dropdown .choices__item--selectable', { hasText: seriesValue })
+ .first()
+ .click();
+ await expect(page.locator('#collana')).toHaveValue(seriesValue);
// Notes
const note = page.locator('#note_varie, textarea[name="note_varie"]');
diff --git a/tests/issue-179-series-autocomplete.spec.js b/tests/issue-179-series-autocomplete.spec.js
index 91baf2a7..220c0af7 100644
--- a/tests/issue-179-series-autocomplete.spec.js
+++ b/tests/issue-179-series-autocomplete.spec.js
@@ -12,7 +12,7 @@ const ADMIN_PASS = process.env.E2E_ADMIN_PASS || '';
let page;
test.beforeAll(async ({ browser }) => {
- test.skip(!ADMIN_EMAIL, 'creds not configured (use /tmp/run-e2e.sh)');
+ test.skip(!ADMIN_EMAIL || !ADMIN_PASS, 'E2E admin credentials not configured (set E2E_ADMIN_EMAIL and E2E_ADMIN_PASS, e.g. via /tmp/run-e2e.sh)');
page = await browser.newPage();
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
@@ -59,6 +59,31 @@ test('Typing a brand-new universe name commits it (create-new path)', async () =
await expect(page.locator('#serie_padre')).toHaveValue(NEW);
});
+test('Enter commits the TYPED value even when a different suggestion is highlighted (#74)', async () => {
+ await page.goto(`${BASE}/admin/books/create`);
+ await page.waitForSelector('#serie_padre_select', { state: 'attached', timeout: 10000 });
+
+ // "Seed: Fairy" is a prefix of the existing universe "Seed: Fairy Tail
+ // Universe": typing it lists BOTH the typed value and the existing one. We
+ // move the highlight onto the EXISTING suggestion (different from the typed
+ // text) and press Enter — the field MUST commit the typed text, not the
+ // highlighted suggestion. Without the _onEnterKey guard this regresses to the
+ // issue #74 bug (wrong value committed).
+ const PARTIAL = 'Seed: Fairy';
+ await universeWrapper().click();
+ const search = universeWrapper().locator('input[type="search"], .choices__input--cloned').first();
+ await search.fill(PARTIAL);
+
+ await expect(
+ page.locator('.choices__list--dropdown .choices__item--selectable', { hasText: 'Seed: Fairy Tail Universe' }).first()
+ ).toBeVisible({ timeout: 8000 });
+ // Highlight moves off the prepended typed option onto the existing universe.
+ await search.press('ArrowDown');
+ await search.press('Enter');
+
+ await expect(page.locator('#serie_padre')).toHaveValue(PARTIAL);
+});
+
// All four series fields share the same generic autocomplete: each suggests its
// own existing values (from the right collane source) and writes the picked
// value to its hidden input.
diff --git a/tests/series-cycles.spec.js b/tests/series-cycles.spec.js
index ff197372..c8476004 100644
--- a/tests/series-cycles.spec.js
+++ b/tests/series-cycles.spec.js
@@ -151,17 +151,43 @@ async function submitBookForm(page, expectedId = null) {
return Number(match?.[1]);
}
-// The universe/group/cycle/series fields are Choices.js autocompletes (#179);
-// their submitted value lives in a hidden input set via the exposed setter.
+// The universe/group/cycle/series fields are Choices.js autocompletes (#179).
+// Drive the REAL widget flow (open → type → pick suggestion) so the test
+// exercises fetch/dropdown/selection, not just the hidden input. The typed
+// value is always offered as a create-option, so this works for new names too.
async function setSeriesAutocomplete(page, field, value) {
- await page.evaluate(([f, v]) => {
- if (window.__seriesAutocomplete && window.__seriesAutocomplete[f]) {
- window.__seriesAutocomplete[f](v);
- } else {
+ const v = String(value || '').trim();
+ const wrapper = page.locator('.choices', { has: page.locator(`#${field}_select`) });
+ const hasWidget = (await wrapper.count()) > 0;
+
+ // Pages without the Choices widget (e.g. the series-detail admin form) expose
+ // a plain with the field id — fill it directly.
+ if (!hasWidget) {
+ const plain = page.locator(`#${field}`);
+ if ((await plain.count()) > 0) await plain.fill(v);
+ return;
+ }
+
+ if (!v) {
+ await page.evaluate((f) => {
const h = document.getElementById(f);
- if (h) h.value = v;
- }
- }, [field, value || '']);
+ if (h) h.value = '';
+ }, field);
+ return;
+ }
+
+ // Book form: drive the real widget per the repo convention
+ // (.coderabbit.yaml) — fill + waitForTimeout + click the suggestion. The typed
+ // value is always offered as a create-option, so this works for new names too.
+ await wrapper.click();
+ const search = wrapper.locator('input[type="search"], .choices__input--cloned').first();
+ await search.fill(v);
+ await page.waitForTimeout(350);
+ await wrapper
+ .locator('.choices__list--dropdown .choices__item--selectable', { hasText: v })
+ .first()
+ .click();
+ await expect(page.locator(`#${field}`)).toHaveValue(v);
}
async function createBookWithSeries(page, data) {