Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions app/Views/libri/partials/book_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,19 +508,19 @@
<?php $gruppoSerieVal = (string)($book['gruppo_serie'] ?? ''); ?>
<select id="gruppo_serie_select" data-series-autocomplete="gruppo_serie" data-placeholder="<?= htmlspecialchars(__('es. Fairy Tail'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="gruppo_serie_help">
<option value=""></option>
<?php if ($gruppoSerieVal !== ''): ?><option value="<?= HtmlHelper::e($gruppoSerieVal) ?>" selected><?= HtmlHelper::e($gruppoSerieVal) ?></option><?php endif; ?>
<?php if ($gruppoSerieVal !== ''): ?><option value="<?= htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
</select>
<input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo HtmlHelper::e($gruppoSerieVal); ?>" />
<input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo htmlspecialchars($gruppoSerieVal, ENT_QUOTES, 'UTF-8'); ?>" />
<p id="gruppo_serie_help" class="text-xs text-gray-500 mt-1"><?= __('Etichetta "ombrello" per spin-off (es. tutto il franchise di Fairy Tail).') ?></p>
</div>
<div>
<label for="serie_padre_select" class="form-label"><?= __("Serie padre / universo") ?></label>
<?php $seriePadreVal = (string)($book['serie_padre'] ?? ''); ?>
<select id="serie_padre_select" data-series-autocomplete="serie_padre" data-placeholder="<?= htmlspecialchars(__('es. I mondi di Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="serie_padre_help">
<option value=""></option>
<?php if ($seriePadreVal !== ''): ?><option value="<?= HtmlHelper::e($seriePadreVal) ?>" selected><?= HtmlHelper::e($seriePadreVal) ?></option><?php endif; ?>
<?php if ($seriePadreVal !== ''): ?><option value="<?= htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
</select>
<input type="hidden" id="serie_padre" name="serie_padre" value="<?php echo HtmlHelper::e($seriePadreVal); ?>" />
<input type="hidden" id="serie_padre" name="serie_padre" value="<?php echo htmlspecialchars($seriePadreVal, ENT_QUOTES, 'UTF-8'); ?>" />
<p id="serie_padre_help" class="text-xs text-gray-500 mt-1"><?= __("Serie superiore nella gerarchia (es. l'universo che contiene cicli e stagioni).") ?></p>
</div>
<div>
Expand All @@ -537,9 +537,9 @@
<?php $collanaVal = (string)($book['collana'] ?? ''); ?>
<select id="collana_select" data-series-autocomplete="collana" data-placeholder="<?= htmlspecialchars(__('es. Fairy Tail: 100 Years Quest'), ENT_QUOTES, 'UTF-8') ?>" class="form-input" aria-describedby="collana_help">
<option value=""></option>
<?php if ($collanaVal !== ''): ?><option value="<?= HtmlHelper::e($collanaVal) ?>" selected><?= HtmlHelper::e($collanaVal) ?></option><?php endif; ?>
<?php if ($collanaVal !== ''): ?><option value="<?= htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
</select>
<input type="hidden" id="collana" name="collana" value="<?php echo HtmlHelper::e($collanaVal); ?>" />
<input type="hidden" id="collana" name="collana" value="<?php echo htmlspecialchars($collanaVal, ENT_QUOTES, 'UTF-8'); ?>" />
<p id="collana_help" class="text-xs text-gray-500 mt-1"><?= __("Nome specifico della serie a cui appartiene il libro.") ?></p>
</div>
</div>
Expand All @@ -554,9 +554,9 @@
<?php $cicloSerieVal = (string)($book['ciclo_serie'] ?? ''); ?>
<select id="ciclo_serie_select" data-series-autocomplete="ciclo_serie" data-placeholder="<?= htmlspecialchars(__('es. Ciclo 1 - Aldebaran'), ENT_QUOTES, 'UTF-8') ?>" class="form-input">
<option value=""></option>
<?php if ($cicloSerieVal !== ''): ?><option value="<?= HtmlHelper::e($cicloSerieVal) ?>" selected><?= HtmlHelper::e($cicloSerieVal) ?></option><?php endif; ?>
<?php if ($cicloSerieVal !== ''): ?><option value="<?= htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8') ?>" selected><?= htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8') ?></option><?php endif; ?>
</select>
<input type="hidden" id="ciclo_serie" name="ciclo_serie" value="<?php echo HtmlHelper::e($cicloSerieVal); ?>" />
<input type="hidden" id="ciclo_serie" name="ciclo_serie" value="<?php echo htmlspecialchars($cicloSerieVal, ENT_QUOTES, 'UTF-8'); ?>" />
</div>
<div>
<label for="ordine_ciclo" class="form-label"><?= __("Ordine ciclo") ?></label>
Expand Down Expand Up @@ -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);
};
Expand Down
26 changes: 16 additions & 10 deletions tests/full-test.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Notes
const note = page.locator('#note_varie, textarea[name="note_varie"]');
Expand Down
27 changes: 26 additions & 1 deletion tests/issue-179-series-autocomplete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
page = await browser.newPage();
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
Expand Down Expand Up @@ -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.
Expand Down
44 changes: 35 additions & 9 deletions tests/series-cycles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <input> 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async function createBookWithSeries(page, data) {
Expand Down
Loading