feat(#8): buscador avanzado nativo (FULLTEXT + filtros)
Replica el «Buscador avanzado» del Joomla viejo sin servicios externos: - fea-search-fulltext.php: motor MySQL FULLTEXT (MATCH AGAINST, Boolean Mode, orden por relevancia); comprueba que existe el índice fea_ft y degrada al buscador nativo si no, sin romper. - fea-search-advanced.php: formulario con filtros por autor, categoría (lista curada de secciones), cita bíblica (meta _cita_evangelio por prefijo) y rango de fechas; pre_get_posts, chips de filtros, byline en tarjetas, i18n es/en/fr/it/pt. Excluye pseudo-autores «Testamento». - create_buscar_page.php: crea la página /buscar + traducciones (idempotente). - tools/e2e: suite Playwright de verificación. Desplegado y verificado en prod (índice fea_ft + página /buscar + traducciones). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* shot_search_advanced.cjs — Verificación del buscador avanzado (#8)
|
||||
* Comprueba: FULLTEXT, filtro autor, filtro tema, filtro cita bíblica,
|
||||
* filtro fecha, combinado palabra+autor, multiidioma, formulario desktop/móvil.
|
||||
*
|
||||
* Uso: NODE_PATH=/home/rafa/joomla-migration/tools/e2e/node_modules node tools/e2e/shot_search_advanced.cjs
|
||||
*/
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const BASE = 'http://localhost:8081';
|
||||
const OUT = '/tmp/claude-1000/-mnt-c-Users-Chia/2802dab4-7a28-4ab7-ad16-457debcccc00/scratchpad';
|
||||
|
||||
// Ensure output dir
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
function shotPath(name) {
|
||||
return path.join(OUT, name + '.png');
|
||||
}
|
||||
|
||||
async function check(page, label, fn) {
|
||||
try {
|
||||
const result = await fn(page);
|
||||
console.log(`✓ ${label}:`, result);
|
||||
} catch (e) {
|
||||
console.error(`✗ ${label}: ERROR`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const b = await chromium.launch({ headless: true });
|
||||
|
||||
// ── 1. FULLTEXT por relevancia ──────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?s=oraci%C3%B3n`, { waitUntil: 'networkidle' });
|
||||
// Comprueba que hay resultados
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ FULLTEXT /?s=oración: ${cards.length} tarjetas`);
|
||||
// Comprueba que el formulario avanzado está presente
|
||||
const form = await p.$('.fea-adv-form');
|
||||
console.log(` formulario avanzado en resultados: ${!!form}`);
|
||||
const counter = await p.$('.fea-adv-count');
|
||||
const countText = counter ? await counter.textContent() : 'no counter';
|
||||
console.log(` contador: ${countText}`);
|
||||
await p.screenshot({ path: shotPath('1_fulltext_oración'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 2. Filtro autor ─────────────────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
// Fray Marcos = 382, Florentino Ulibarri = 395
|
||||
await p.goto(`${BASE}/?s=&fea_author=382`, { waitUntil: 'networkidle' });
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ Filtro autor (fea_author=382 Fray Marcos): ${cards.length} tarjetas`);
|
||||
// Verify selected value in form
|
||||
const selVal = await p.$eval('#fea-author', el => el.value).catch(() => 'n/a');
|
||||
console.log(` select author value: ${selVal}`);
|
||||
await p.screenshot({ path: shotPath('2_filtro_autor'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 3. Filtro categoría + verificación del desplegable ───────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?fea_cat=1650`, { waitUntil: 'networkidle' });
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ Filtro categoría (fea_cat=1650 Artículos): ${cards.length} tarjetas`);
|
||||
const selVal = await p.$eval('#fea-cat', el => el.value).catch(() => 'n/a');
|
||||
console.log(` select cat value: ${selVal}`);
|
||||
// Label debe decir "Categoría"
|
||||
const catLabel = await p.$eval('label[for="fea-cat"]', el => el.textContent).catch(() => 'n/a');
|
||||
console.log(` label categoría: "${catLabel}"`);
|
||||
// Contar y listar opciones de categoría (idioma ES)
|
||||
const catOpts = await p.$$eval('#fea-cat option', els => els.map(e => e.textContent));
|
||||
console.log(` nº opciones categoría: ${catOpts.length}`);
|
||||
console.log(` opciones: ${catOpts.join(' | ')}`);
|
||||
// No debe contener categorías de carta ni "Testament"
|
||||
const badCat = catOpts.filter(t => /Testament/i.test(t));
|
||||
console.log(` opciones con 'Testament': ${badCat.length}`);
|
||||
await p.screenshot({ path: shotPath('3_filtro_categoria'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 3b. Desplegable de AUTOR: sin "Testamento" ───────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||
const autOpts = await p.$$eval('#fea-author option', els => els.map(e => e.textContent));
|
||||
console.log(`✓ Desplegable autor: ${autOpts.length} opciones`);
|
||||
const badAut = autOpts.filter(t => /Testament/i.test(t));
|
||||
console.log(` opciones autor con 'Testament': ${badAut.length} ${badAut.length ? '→ ' + badAut.join(', ') : ''}`);
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 4. Filtro cita bíblica (PREFIJO) ─────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?fea_cita=Jn`, { waitUntil: 'networkidle' });
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ Filtro cita PREFIJO (fea_cita=Jn): ${cards.length} tarjetas`);
|
||||
const citaVal = await p.$eval('#fea-cita', el => el.value).catch(() => 'n/a');
|
||||
console.log(` input cita value: ${citaVal}`);
|
||||
const chip = await p.$('.fea-adv-chip');
|
||||
const chipText = chip ? await chip.textContent() : 'no chip';
|
||||
console.log(` chip activo: ${chipText}`);
|
||||
// Placeholder corto + foco para ver que NO se corta
|
||||
const ph = await p.$eval('#fea-cita', el => el.placeholder).catch(() => 'n/a');
|
||||
console.log(` placeholder cita: "${ph}"`);
|
||||
await p.screenshot({ path: shotPath('4_filtro_cita'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 4b. Cita bíblica con valor largo: comprobar que no desborda ──────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?fea_cita=Jn%2020%2C%2019-31`, { waitUntil: 'networkidle' });
|
||||
// Medir overflow: scrollWidth vs clientWidth del input y de su fila
|
||||
const metrics = await p.$eval('#fea-cita', el => ({
|
||||
scrollW: el.scrollWidth, clientW: el.clientWidth,
|
||||
offsetW: el.offsetWidth, parentW: el.parentElement.clientWidth,
|
||||
})).catch(() => null);
|
||||
console.log(`✓ Cita valor largo (Jn 20, 19-31): ${JSON.stringify(metrics)}`);
|
||||
const overflow = metrics ? (metrics.offsetW > metrics.parentW + 1) : 'n/a';
|
||||
console.log(` input desborda su celda: ${overflow}`);
|
||||
await p.screenshot({ path: shotPath('4b_cita_largo_nocorta'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 5. Filtro fecha ─────────────────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?fea_date_from=2020-01-01&fea_date_to=2020-12-31`, { waitUntil: 'networkidle' });
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ Filtro fecha (2020): ${cards.length} tarjetas`);
|
||||
await p.screenshot({ path: shotPath('5_filtro_fecha'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 6. Combinado palabra + autor ────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?s=amor&fea_author=384`, { waitUntil: 'networkidle' }); // Enrique Martínez Lozano
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
console.log(`✓ Combinado s=amor + fea_author=384 (Enrique M.L.): ${cards.length} tarjetas`);
|
||||
await p.screenshot({ path: shotPath('6_combinado_palabra_autor'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 7. Multiidioma /en/?s=love ──────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/en/?s=love`, { waitUntil: 'networkidle' });
|
||||
const cards = await p.$$('.fea-archive-card');
|
||||
// Check form action points to /en/
|
||||
const formAction = await p.$eval('.fea-adv-form', el => el.action).catch(() => 'n/a');
|
||||
console.log(`✓ Multiidioma /en/?s=love: ${cards.length} tarjetas, form action: ${formAction}`);
|
||||
// Check if labels are in English
|
||||
const firstLabel = await p.$eval('.fea-adv-label', el => el.textContent).catch(() => 'n/a');
|
||||
console.log(` primer label: ${firstLabel}`);
|
||||
await p.screenshot({ path: shotPath('7_multiidioma_en'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 8a. Formulario visible en desktop ───────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||
const formVisible = await p.isVisible('.fea-adv-form');
|
||||
console.log(`✓ Formulario desktop (1280px): visible=${formVisible}`);
|
||||
await p.screenshot({ path: shotPath('8a_form_desktop'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 8b. Formulario visible en móvil ────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 390, height: 844 } });
|
||||
await p.goto(`${BASE}/?s=fe`, { waitUntil: 'networkidle' });
|
||||
const formVisible = await p.isVisible('.fea-adv-form');
|
||||
console.log(`✓ Formulario móvil (390px): visible=${formVisible}`);
|
||||
// Check advanced link in mobile header
|
||||
const advLink = await p.$('.fea-adv-link');
|
||||
const advLinkVisible = advLink ? await advLink.isVisible() : false;
|
||||
console.log(` enlace búsqueda avanzada móvil: visible=${advLinkVisible}`);
|
||||
await p.screenshot({ path: shotPath('8b_form_mobile'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
// ── 9. Página /buscar ────────────────────────────────────────────────────
|
||||
{
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await p.goto(`${BASE}/buscar/`, { waitUntil: 'networkidle' });
|
||||
const status = p.url();
|
||||
const form = await p.$('.fea-adv-form');
|
||||
console.log(`✓ Página /buscar: url=${status}, form=${!!form}`);
|
||||
await p.screenshot({ path: shotPath('9_pagina_buscar'), fullPage: false });
|
||||
await p.close();
|
||||
}
|
||||
|
||||
await b.close();
|
||||
console.log(`\nCapturas guardadas en: ${OUT}`);
|
||||
})();
|
||||
Reference in New Issue
Block a user