Files
feadulta/tools/e2e/shot_search_advanced.cjs
T
rafa e2f23ace9f 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>
2026-06-27 18:24:53 -04:00

205 lines
10 KiB
JavaScript

/**
* 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}`);
})();