Skip to content

feat(books): autocomplete for the series/universe/cycle fields (#179)#187

Merged
fabiodalez-dev merged 2 commits into
mainfrom
feat/179-series-universe-picker
Jun 23, 2026
Merged

feat(books): autocomplete for the series/universe/cycle fields (#179)#187
fabiodalez-dev merged 2 commits into
mainfrom
feat/179-series-universe-picker

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Closes #179. The cycle/universe/series book-form fields were free-text — the user retyped the universe every time and a typo silently spawned a duplicate universe. They now get the same Choices.js autocomplete as the author/publisher pickers.

BackendCollaneController::searchApi gains a whitelisted field param mapping each field to its real source: serie_padre→existing universes (collane.nome WHERE tipo='universo'), gruppo_seriecollane.gruppo_serie, ciclo_seriecollane.ciclo, collanacollane.nome+libri.collana. Tables/columns whitelisted, values bound.

Frontend — each field is a single-select Choices mirrored into a hidden input (submits unchanged). Typing ≥2 letters proposes existing values; the typed text is always offered + committed on blur/Enter so a new value can still be created. The ISBN scraper fills collana via the exposed setter. New i18n key ×4 locales.

Tests — new issue-179 regression (suggest existing + create new, all 4 fields); series-cycles + full-test migrated. Validated: PHPStan L5, issue-179 5/5, series-cycles 15/15.

Summary by CodeRabbit

  • New Features

    • Aggiunto autocompletamento intelligente per i campi Serie, Gruppo, Ciclo e Collana nel modulo di creazione/modifica libri, con ricerca server-side in tempo reale.
    • Possibilità di creare nuove voci direttamente durante la ricerca.
  • Documentation

    • Aggiornate traduzioni in tedesco, inglese, francese e italiano con messaggi per il nuovo autocompletamento.
  • Tests

    • Aggiunti test E2E per validare il comportamento degli autocompletamenti sui campi serie.

The cycle/universe/series fields on the book form were free-text inputs: a user
had to retype the universe name for every book, and a single different character
silently created a brand-new universe (or the wrong field got filled). Give the
four fields the same Choices.js shape as the author/publisher pickers.

Backend - CollaneController::searchApi gains a `field` param (whitelisted) that
maps each field to its real suggestion source:
- collana      -> collane.nome (tipo=serie) + libri.collana
- serie_padre  -> collane.nome WHERE tipo='universo'  (existing universes)
- gruppo_serie -> collane.gruppo_serie
- ciclo_serie  -> collane.ciclo
Tables/columns are whitelisted (never from input); the query value and tipo are
bound.

Frontend - each field becomes a single-select Choices.js mirrored into a hidden
input that submits its value unchanged. Typing >=2 letters proposes existing
values; the typed text is always offered too (and committed on blur/Enter) so a
genuinely new value can still be created. The ISBN scraper fills collana through
the exposed setter so the value shows in the control. New i18n key in all 4
locales.

Tests: new issue-179 regression spec (suggest existing + create new, all 4
fields); series-cycles + full-test migrated to set these via the autocomplete.
Validated: PHPStan L5, issue-179 5/5, series-cycles 15/15.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fabiodalez-dev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 3 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 11f81602-f126-4da2-aeea-5d3640facf88

📥 Commits

Reviewing files that changed from the base of the PR and between cd46a25 and ebfb1c8.

📒 Files selected for processing (1)
  • app/Controllers/CollaneController.php
📝 Walkthrough

Walkthrough

Sostituisce i campi testuali serie/collana nel form libro con autocomplete Choices.js (gruppo_serie, serie_padre, collana, ciclo_serie). Il backend searchApi viene riscritto con costruzione dinamica SQL (whitelist di sorgenti, UNION parametrizzata). Aggiunge traduzioni per la nuova stringa di prompt e aggiorna i test E2E Playwright.

Modifiche

Autocomplete serie/collana (issue #179)

Layer / File(s) Riepilogo
Backend: riscrittura searchApi con query dinamica
app/Controllers/CollaneController.php
Sostituisce i branch statici con una whitelist di sorgenti per campo (collana, serie_padre, gruppo_serie, ciclo_serie) e costruisce iterativamente $sqlParts/$binds, producendo una UNION parametrizzata con SELECT DISTINCT, ORDER BY e LIMIT 10.
Form libro: select Choices.js + input hidden
app/Views/libri/partials/book_form.php
Sostituisce i quattro campi testuali con coppie <select data-series-autocomplete> + <input type="hidden">, precompilando l'option selezionata dai valori del modello.
JS: initializeSeriesAutocompletes e integrazione ISBN scraping
app/Views/libri/partials/book_form.php
Aggiunge initializeSeriesAutocompletes() (ricerca server-side su /api/collane/search, sync input hidden, window.__seriesAutocomplete[field] setter, blur-commit, monkey-patch _onEnterKey). Aggiorna l'handler ISBN scraping per usare il setter autocomplete.
Traduzioni: nuova stringa 2-lettere minimo
locale/it_IT.json, locale/en_US.json, locale/de_DE.json, locale/fr_FR.json
Aggiunge la stringa "Digita almeno 2 lettere per cercare o creare" nelle quattro lingue e aggiorna la stringa upload_max_filesize.
Test E2E: nuova spec autocomplete + aggiornamento spec esistenti
tests/issue-179-series-autocomplete.spec.js, tests/full-test.spec.js, tests/series-cycles.spec.js
Aggiunge spec Playwright per selezione esistente, creazione nuovo valore e loop su tutti i campi serie. Aggiorna le spec esistenti per usare page.evaluate/window.__seriesAutocomplete al posto di page.fill.

Sequence Diagram

sequenceDiagram
  actor Utente
  participant BookForm as book_form.php (JS)
  participant CollaneAPI as /api/collane/search
  participant DB as Database

  Utente->>BookForm: digita ≥2 caratteri in select[data-series-autocomplete]
  BookForm->>CollaneAPI: GET ?field=collana&q=xyz
  CollaneAPI->>CollaneAPI: valida field, escaping LIKE, itera sorgenti
  CollaneAPI->>DB: SELECT DISTINCT … FROM (UNION …) LIMIT 10
  DB-->>CollaneAPI: righe corrispondenti
  CollaneAPI-->>BookForm: JSON array di suggerimenti
  BookForm->>BookForm: aggiorna opzioni Choices.js (+ opzione "digita nuovo")
  Utente->>BookForm: seleziona opzione o preme Enter
  BookForm->>BookForm: sincronizza input[hidden] con valore scelto
  BookForm->>BookForm: window.__seriesAutocomplete[field] = setter
Loading

Stima dello sforzo di revisione

🎯 4 (Complex) | ⏱️ ~60 minutes

PR potenzialmente correlate

  • fabiodalez-dev/Pinakes#95: Introduce il metodo searchApi in CollaneController.php che questa PR riscrive completamente con query dinamica parametrizzata.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo descrive accuratamente la modifica principale: aggiunge autocomplete ai campi serie/universo/ciclo nel form dei libri, in linea con l'issue #179.
Linked Issues check ✅ Passed La PR implementa completamente i requisiti dell'issue #179: autocomplete per universo/ciclo/serie, prevenzione di duplicati, e supporto per valori nuovi tramite selezione Choices.js.
Out of Scope Changes check ✅ Passed Tutte le modifiche rientrano nell'ambito della feature #179: backend autocomplete, frontend Choices.js, traduzioni i18n, e aggiornamento dei test correlati.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/179-series-universe-picker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Views/libri/partials/book_form.php`:
- Around line 511-513: Replace all instances of HtmlHelper::e() with
htmlspecialchars(..., ENT_QUOTES, 'UTF-8') in the book_form.php view file. This
applies to all occurrences of HtmlHelper::e($gruppoSerieVal) in the option value
attribute, option text content, and hidden input value attribute across the
specified line ranges (511-513, 521-523, 540-542, 557-559). According to the
codebase conventions, HtmlHelper::e() should not be used in view files; instead
use the native PHP htmlspecialchars function with the appropriate ENT_QUOTES and
UTF-8 parameters for proper HTML escaping.
- Around line 2229-2237: The _onEnterKey override for the series field does not
handle the case where the user types a value different from the highlighted
dropdown item. The current logic only commits the typed value when there is no
highlighted item, but delegates to origEnter when a highlighted item exists,
which incorrectly selects the wrong suggestion. Extract the text content of the
highlighted item (hl), compare it with the typed value, and if they differ, call
event.preventDefault() and commitTyped(typed) to prevent the wrong suggestion
from being selected. This should mirror the same pattern already correctly
implemented in initializePublishersChoices() for publisher selection, ensuring
the user's typed input is preserved instead of the highlighted dropdown
suggestion when they differ.

In `@tests/full-test.spec.js`:
- Around line 690-699: The collana field configuration in the evaluate block
bypasses the actual Choices.js autocomplete interaction flow by directly calling
the setter or setting the hidden input value, which means the test does not
verify that the autocomplete widget functions correctly. Replace the
page.evaluate approach with actual user interactions: use fill to input the
value into the collana field, add a waitForTimeout to allow suggestions to
appear, and then click on the matching suggestion from the dropdown to properly
simulate user interaction with the Choices.js autocomplete widget, consistent
with how other autocomplete fields are tested elsewhere in the test suite.

In `@tests/issue-179-series-autocomplete.spec.js`:
- Around line 15-20: The test.skip condition at the beginning of the suite only
validates ADMIN_EMAIL but does not validate ADMIN_PASS, which means the test
will run and fail during the login attempt if the password credential is
missing. Extend the skip condition to check that both ADMIN_EMAIL and ADMIN_PASS
environment variables are properly configured before allowing the test to
execute, ensuring complete credential validation before attempting the page.fill
calls for email and password fields.

In `@tests/series-cycles.spec.js`:
- Around line 154-166: The setSeriesAutocomplete function bypasses the actual
Choices.js end-to-end flow by directly injecting values via page.evaluate and
hidden input manipulation, which means tests can pass even if Choices.js
functionality like fetching suggestions, dropdown opening, or selection is
broken. Replace the current implementation with a simulation of the real user
interaction flow: fill the input field with the value, wait briefly for
suggestions to appear, and then click on the matching suggestion. This updated
approach should then be reused consistently wherever setSeriesAutocomplete is
called throughout the test file (in the test cases around lines 171-176, 351,
and 395).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89656ffb-ccc6-4bc7-8f8b-8f52dc08f83b

📥 Commits

Reviewing files that changed from the base of the PR and between 8784c85 and cd46a25.

📒 Files selected for processing (9)
  • app/Controllers/CollaneController.php
  • app/Views/libri/partials/book_form.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/full-test.spec.js
  • tests/issue-179-series-autocomplete.spec.js
  • tests/series-cycles.spec.js

Comment on lines +511 to +513
<?php if ($gruppoSerieVal !== ''): ?><option value="<?= HtmlHelper::e($gruppoSerieVal) ?>" selected><?= HtmlHelper::e($gruppoSerieVal) ?></option><?php endif; ?>
</select>
<input type="hidden" id="gruppo_serie" name="gruppo_serie" value="<?php echo HtmlHelper::e($gruppoSerieVal); ?>" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sostituisci HtmlHelper::e() nei nuovi campi autocomplete.

Questi valori finiscono in attributi HTML e testo visibile; le nuove righe devono usare direttamente htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

Correzione proposta
-                  <?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'); ?>" />
...
-                  <?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'); ?>" />
...
-                  <?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'); ?>" />
...
-                  <?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'); ?>" />

As per path instructions, Mai usare HtmlHelper::e() nelle view — usare htmlspecialchars(..., ENT_QUOTES, 'UTF-8').

Also applies to: 521-523, 540-542, 557-559

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/libri/partials/book_form.php` around lines 511 - 513, Replace all
instances of HtmlHelper::e() with htmlspecialchars(..., ENT_QUOTES, 'UTF-8') in
the book_form.php view file. This applies to all occurrences of
HtmlHelper::e($gruppoSerieVal) in the option value attribute, option text
content, and hidden input value attribute across the specified line ranges
(511-513, 521-523, 540-542, 557-559). According to the codebase conventions,
HtmlHelper::e() should not be used in view files; instead use the native PHP
htmlspecialchars function with the appropriate ENT_QUOTES and UTF-8 parameters
for proper HTML escaping.

Source: Path instructions

Comment on lines +2229 to +2237
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
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;
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify file exists and check its structure
if [ -f "app/Views/libri/partials/book_form.php" ]; then
  wc -l "app/Views/libri/partials/book_form.php"
  echo "---"
  # Get lines around 2229-2237 to see the actual code
  sed -n '2200,2250p' "app/Views/libri/partials/book_form.php"
else
  echo "File not found"
  find . -name "book_form.php" -type f
fi

Repository: fabiodalez-dev/Pinakes

Length of output: 3063


🏁 Script executed:

# Search for Choices library usage and DOM structure references
rg "choices__item" "app/Views" -A 2 -B 2

echo "---"

# Search for any other references to .choices__item-text
rg "choices__item-text" . --type html --type php --type js

echo "---"

# Check if Choices is imported and what version/configuration
rg "Choices\|choices\.js" "app/Views/libri/partials/book_form.php" -B 5 -A 5 | head -30

Repository: fabiodalez-dev/Pinakes

Length of output: 8039


🏁 Script executed:

# Get exact context around line 2229 to confirm if it's still the unfixed version
sed -n '2215,2245p' "app/Views/libri/partials/book_form.php" | cat -n

Repository: fabiodalez-dev/Pinakes

Length of output: 2030


Il campo serie committerà il suggerimento sbagliato se l'utente digita un nome diverso da quello evidenziato e preme Enter.

Quando la lista dropdown ha un elemento evidenziato (.is-highlighted), il codice delega a origEnter() che può selezionare il suggerimento sbagliato, anche se l'utente ha digitato un valore diverso. Questo causa il salvataggio di dati errati nel campo hidden.

La correzione è già implementata correttamente in initializePublishersChoices() (estrae il testo evidenziato, lo confronta con quello digitato, previene il default se diverso). Applica lo stesso pattern al metodo _onEnterKey della serie autocomplete.

Correzione proposta
                choice._onEnterKey = function (event, hasActiveDropdown) {
                    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;
-                    if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
+                    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);
                };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
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;
if (typed && !hl) { event.preventDefault(); commitTyped(typed); return; }
return origEnter(event, hasActiveDropdown);
};
if (typeof choice._onEnterKey === 'function') {
const origEnter = choice._onEnterKey.bind(choice);
choice._onEnterKey = function (event, hasActiveDropdown) {
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;
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);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/libri/partials/book_form.php` around lines 2229 - 2237, The
_onEnterKey override for the series field does not handle the case where the
user types a value different from the highlighted dropdown item. The current
logic only commits the typed value when there is no highlighted item, but
delegates to origEnter when a highlighted item exists, which incorrectly selects
the wrong suggestion. Extract the text content of the highlighted item (hl),
compare it with the typed value, and if they differ, call event.preventDefault()
and commitTyped(typed) to prevent the wrong suggestion from being selected. This
should mirror the same pattern already correctly implemented in
initializePublishersChoices() for publisher selection, ensuring the user's typed
input is preserved instead of the highlighted dropdown suggestion when they
differ.

Comment thread tests/full-test.spec.js
Comment on lines +690 to +699
// 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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Impostazione collana via evaluate bypassa il controllo reale dell’autocomplete

In Lines 692-699 il test non passa dal dropdown/suggestion flow di Choices.js, quindi può non rilevare regressioni UI sull’autocomplete della collana. Conviene usare interazione utente (fill, waitForTimeout, click suggestion) anche qui.

🔧 Fix proposto
-    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}`);
+    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 page
+      .locator('.choices__list--dropdown .choices__item--selectable', { hasText: seriesValue })
+      .first()
+      .click();

As per path instructions, nei test con Choices.js va usato fill + waitForTimeout + click suggestion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/full-test.spec.js` around lines 690 - 699, The collana field
configuration in the evaluate block bypasses the actual Choices.js autocomplete
interaction flow by directly calling the setter or setting the hidden input
value, which means the test does not verify that the autocomplete widget
functions correctly. Replace the page.evaluate approach with actual user
interactions: use fill to input the value into the collana field, add a
waitForTimeout to allow suggestions to appear, and then click on the matching
suggestion from the dropdown to properly simulate user interaction with the
Choices.js autocomplete widget, consistent with how other autocomplete fields
are tested elsewhere in the test suite.

Source: Path instructions

Comment on lines +15 to +20
test.skip(!ADMIN_EMAIL, 'creds not configured (use /tmp/run-e2e.sh)');
page = await browser.newPage();
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.click('button[type="submit"]');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guardia credenziali incompleta nel bootstrap della suite

A Line 15 lo skip controlla solo E2E_ADMIN_EMAIL: se E2E_ADMIN_PASS è vuota, i test non vengono skippati e falliscono nel login. Conviene estendere la condizione di skip almeno a email+password admin.

🔧 Fix proposto
 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)',
+  );
   page = await browser.newPage();

Based on learnings, in questo repository il gating E2E deve basarsi sulle variabili d’ambiente CI realmente iniettate, non su condizioni parziali.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test.skip(!ADMIN_EMAIL, 'creds not configured (use /tmp/run-e2e.sh)');
page = await browser.newPage();
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.click('button[type="submit"]');
test.skip(
!ADMIN_EMAIL || !ADMIN_PASS,
'E2E admin credentials not configured (set E2E_ADMIN_EMAIL and E2E_ADMIN_PASS)',
);
page = await browser.newPage();
await page.goto(`${BASE}/accedi`);
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.click('button[type="submit"]');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/issue-179-series-autocomplete.spec.js` around lines 15 - 20, The
test.skip condition at the beginning of the suite only validates ADMIN_EMAIL but
does not validate ADMIN_PASS, which means the test will run and fail during the
login attempt if the password credential is missing. Extend the skip condition
to check that both ADMIN_EMAIL and ADMIN_PASS environment variables are properly
configured before allowing the test to execute, ensuring complete credential
validation before attempting the page.fill calls for email and password fields.

Source: Learnings

Comment on lines +154 to +166
// The universe/group/cycle/series fields are Choices.js autocompletes (#179);
// their submitted value lives in a hidden input set via the exposed setter.
async function setSeriesAutocomplete(page, field, value) {
await page.evaluate(([f, v]) => {
if (window.__seriesAutocomplete && window.__seriesAutocomplete[f]) {
window.__seriesAutocomplete[f](v);
} else {
const h = document.getElementById(f);
if (h) h.value = v;
}
}, [field, value || '']);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

setSeriesAutocomplete bypassa il percorso E2E reale di Choices.js

In Lines 157-164 il valore viene iniettato via page.evaluate/hidden input: così i test possono passare anche se fetch suggerimenti, dropdown o selezione sono rotti. Qui va simulato il flusso utente (apertura wrapper, fill, attesa breve, click su suggestion) e poi riusato nelle chiamate di Lines 171-176, 351, 395.

🔧 Fix proposto
 async function setSeriesAutocomplete(page, field, value) {
-  await page.evaluate(([f, v]) => {
-    if (window.__seriesAutocomplete && window.__seriesAutocomplete[f]) {
-      window.__seriesAutocomplete[f](v);
-    } else {
-      const h = document.getElementById(f);
-      if (h) h.value = v;
-    }
-  }, [field, value || '']);
+  const v = String(value || '').trim();
+  if (!v) {
+    await page.evaluate((f) => {
+      const h = document.getElementById(f);
+      if (h) h.value = '';
+    }, field);
+    return;
+  }
+
+  const sel = `#${field}_select`;
+  const wrapper = page.locator('.choices', { has: page.locator(sel) });
+  await wrapper.click();
+  const search = wrapper.locator('input[type="search"], .choices__input--cloned').first();
+  await search.fill(v);
+  await page.waitForTimeout(350);
+  await page
+    .locator('.choices__list--dropdown .choices__item--selectable', { hasText: v })
+    .first()
+    .click();
 }

As per path instructions, nei test con Choices.js va usato fill + waitForTimeout + click suggestion.

Also applies to: 167-176, 351-351, 395-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/series-cycles.spec.js` around lines 154 - 166, The
setSeriesAutocomplete function bypasses the actual Choices.js end-to-end flow by
directly injecting values via page.evaluate and hidden input manipulation, which
means tests can pass even if Choices.js functionality like fetching suggestions,
dropdown opening, or selection is broken. Replace the current implementation
with a simulation of the real user interaction flow: fill the input field with
the value, wait briefly for suggestions to appear, and then click on the
matching suggestion. This updated approach should then be reused consistently
wherever setSeriesAutocomplete is called throughout the test file (in the test
cases around lines 171-176, 351, and 395).

Source: Path instructions

@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

Espanso il PHPDoc di searchApi per documentare il nuovo parametro field e il contratto whitelist (commit ebfb1c8), che era il punto della docstring-coverage. Nessun rilievo inline da affrontare.

@fabiodalez-dev fabiodalez-dev merged commit 64a198d into main Jun 23, 2026
6 checks passed
@fabiodalez-dev fabiodalez-dev deleted the feat/179-series-universe-picker branch June 23, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Series/universe review

1 participant