Files
TenerifeProp/tests/scripts/admin-panel-deep-test.js
APAW Agent Sync 8c1b897b9d feat(admin): replace prompt() with Bootstrap modals for CRUD operations
Replace browser prompt()-based editing with proper Bootstrap 5 modal
dialogs for testimonials, services, FAQs, and leads. This provides
better UX with form validation, structured input fields, and i18n
support (ES/RU) instead of raw prompt dialogs.

- Add testimonialModal, serviceModal, faqModal, leadModal to admin.html
- Add show*/save* methods in admin.js for each entity type
- Wire leads.html 'Add lead' button to leadModal
- Add modal JS modules (FAQModal, LeadModal, ServiceModal)
- Add unit and e2e tests for modals and API client
2026-05-16 00:43:04 +01:00

222 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
/// Admin Panel Deep Functional Test v6 — navigates via window.admin.navigateTo()
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:3003';
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@tenerifeprop.com';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin@2026!';
const EXE_PATH = process.env.PLAYWRIGHT_EXECUTABLE_PATH;
const REPORT_DIR = path.join(__dirname, '../reports');
const SCREENSHOT_DIR = path.join(__dirname, '../visual/admin');
const SECTIONS = [
{ name: 'dashboard', label: 'Dashboard', expected_modals: 0 },
{ name: 'properties', label: 'Propiedades', expected_modals: 1 },
{ name: 'leads', label: 'Leads', expected_modals: 0 },
{ name: 'testimonials', label: 'Testimonios', expected_modals: 1 },
{ name: 'services', label: 'Servicios', expected_modals: 1 },
{ name: 'faq', label: 'FAQ', expected_modals: 1 },
{ name: 'users', label: 'Usuarios', expected_modals: 1 },
{ name: 'settings', label: 'Configuracion', expected_modals: 0 },
{ name: 'analytics', label: 'Analytics', expected_modals: 0 },
{ name: 'traffic', label: 'Trafico', expected_modals: 0 },
];
let results = { timestamp: new Date().toISOString(), targetUrl: TARGET_URL, summary: { passed:0, failed:0, warnings:0 }, sections: [] };
function ensure() {
[REPORT_DIR, SCREENSHOT_DIR].forEach(d => { if(!fs.existsSync(d)) fs.mkdirSync(d,{recursive:true}); });
}
function includesAny(text, words) {
const t = (text||'').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
return words.some(w => t.includes(w));
}
async function navigateToSection(page, name) {
await page.evaluate((n) => { if (window.admin) window.admin.navigateTo(n); }, name);
await page.waitForTimeout(5000);
}
async function testSection(page, sec) {
const res = { name:sec.name, label:sec.label, status:'ok', errors:[], buttons:[], actions:[], tables:[], modals:[], verdict:'pass' };
console.log(`\n📄 ${sec.label}`);
const consoleMsgs=[];
const onConsole = msg => { if(msg.type()==='error'||/error|uncaught|typeerror|referenceerror|cannot read|failed to fetch|networkerror/i.test(msg.text())) consoleMsgs.push(msg.text()); };
page.on('console', onConsole);
try {
await navigateToSection(page, sec.name);
res.status='loaded';
} catch(e) { res.status='nav-failed'; res.errors.push(e.message); res.verdict='fail'; page.off('console',onConsole); return res; }
// Collect elements via evaluate
const data = await page.evaluate(() => {
const out={buttons:[],tables:[],modals:[]};
document.querySelectorAll('button, a.btn, .btn, [data-bs-toggle="modal"]').forEach(el => {
const rect=el.getBoundingClientRect();
if(rect.width<=0||rect.height<=0) return;
const txt=(el.textContent||'').trim();
if(!txt) return;
out.buttons.push({text:txt.substring(0,60), target:el.getAttribute('data-bs-target')||''});
});
document.querySelectorAll('table').forEach(t => {
if(t.offsetParent===null) return;
out.tables.push({rows:t.querySelectorAll('tbody tr').length});
});
document.querySelectorAll('.modal').forEach(m => {
const t=m.querySelector('.modal-title');
out.modals.push({id:m.id, title:t?(t.textContent||'').trim().substring(0,60):''});
});
return out;
});
res.buttons=data.buttons;
res.tables=data.tables;
res.modals=data.modals;
// Test add modals by clicking "Añadir" / "Nuevo" / "Agregar"
const addWords=['anadir','nuevo','nueva','crear','agregar'];
const addTriggers=data.buttons.filter(b => includesAny(b.text, addWords));
for(const trig of addTriggers) {
try {
const clicked = await page.evaluate((text) => {
const els=document.querySelectorAll('button, a.btn, .btn');
for(const el of els) {
const t=(el.textContent||'').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
const q=text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
if(t.includes(q)) { el.click(); return true; }
}
return false;
}, trig.text);
if(!clicked) { res.actions.push({type:'btn_not_found', trigger:trig.text}); continue; }
await page.waitForTimeout(1500);
const modal = await page.evaluate(() => {
const m=document.querySelector('.modal.show');
return m?{id:m.id, title:(m.querySelector('.modal-title')?.textContent||'').trim()}:null;
});
if(modal) {
res.actions.push({type:'modal_opened', trigger:trig.text, modal_id:modal.id, modal_title:modal.title, opened:true});
await page.evaluate(() => {
const close=document.querySelector('.modal.show [data-bs-dismiss="modal"], .modal.show .btn-close');
if(close) close.click();
});
await page.waitForTimeout(700);
} else {
res.actions.push({type:'modal_not_opened', trigger:trig.text, opened:false});
}
} catch(e) { res.actions.push({type:'modal_error', trigger:trig.text, error:e.message}); }
}
// Row actions
const rowWords=['ver','editar','eliminar','detalles'];
for(const rw of rowWords) {
try {
await page.evaluate((w) => {
const els=document.querySelectorAll('button, a.btn, .table-action-btn');
for(const el of els) {
const t=(el.textContent||'').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
if(t===w) { el.click(); return; }
}
}, rw);
await page.waitForTimeout(1000);
const overlay=await page.evaluate(() => document.querySelector('.modal.show, .swal2-modal, .toast')!==null);
res.actions.push({type:'row_action', trigger:rw, overlay});
if(overlay) {
await page.evaluate(() => {
const c=document.querySelector('.modal.show [data-bs-dismiss="modal"], .swal2-close, .toast .btn-close');
if(c) c.click();
});
await page.waitForTimeout(500);
}
} catch(e) { res.actions.push({type:'row_action_error', trigger:rw, error:e.message}); }
}
// Filter
const filterBtn = data.buttons.find(b => includesAny(b.text, ['filtrar','buscar','aplicar']));
if(filterBtn) {
try {
await page.evaluate((ft) => {
const els=document.querySelectorAll('button, a.btn, .btn');
for(const el of els) {
const t=(el.textContent||'').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
if(t.includes(ft)) { el.click(); return; }
}
}, filterBtn.text);
await page.waitForTimeout(1000);
res.actions.push({type:'filter_click', trigger:filterBtn.text});
} catch(e) { res.actions.push({type:'filter_error', trigger:filterBtn.text, error:e.message}); }
}
// Screenshot
const ss=path.join(SCREENSHOT_DIR, `${sec.name}.png`);
await page.screenshot({path:ss, fullPage:false});
res.screenshot=ss;
page.off('console', onConsole);
res.consoleErrors=[...new Set(consoleMsgs)];
const critical=res.consoleErrors.some(e => /uncaught|typeerror|referenceerror|cannot read|failed to fetch|networkerror/i.test(e));
const modalOk=res.actions.filter(a => a.type==='modal_opened' && a.opened).length;
if(critical) res.verdict='fail';
else if(sec.expected_modals>0 && modalOk===0) res.verdict='warning';
else res.verdict='pass';
return res;
}
async function run() {
ensure();
console.log(`🔍 Admin Panel Deep Functional Test v6 — ${TARGET_URL}`);
const browser=await chromium.launch({headless:true, executablePath:EXE_PATH});
const page=await browser.newPage({viewport:{width:1920,height:1080}});
console.log('\n🔐 Logging in...');
await page.goto(`${TARGET_URL}/login`, {waitUntil:'domcontentloaded', timeout:15000});
await page.waitForTimeout(1000);
await page.fill('input#email', ADMIN_EMAIL);
await page.fill('input#password', ADMIN_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
console.log(`✅ Logged in: ${page.url()}`);
// Navigate to admin SPA root once
await page.goto(`${TARGET_URL}/admin`, {waitUntil:'domcontentloaded', timeout:15000});
await page.waitForTimeout(2000);
for(const sec of SECTIONS) {
const r=await testSection(page, sec);
results.sections.push(r);
results.summary[r.verdict==='pass'?'passed':r.verdict==='warning'?'warnings':'failed']++;
}
await browser.close();
const rp=path.join(REPORT_DIR, 'admin-panel-deep-report.json');
fs.writeFileSync(rp, JSON.stringify(results, null, 2));
console.log(`\n📊 Report: ${rp}`);
console.log(`\n========== RESULTS ==========`);
for(const s of results.sections) {
const icon=s.verdict==='pass'?'✅':s.verdict==='warning'?'⚠️':'❌';
const modalOk=s.actions.filter(a => a.type==='modal_opened' && a.opened).length;
const modalTotal=s.actions.filter(a => a.type==='modal_opened').length;
console.log(`${icon} ${s.label}: ${s.verdict} | btns=${s.buttons.length} modals=${modalOk}/${modalTotal} tables=${s.tables.length} errors=${s.consoleErrors.length}`);
for(const a of s.actions) {
const ai=a.type==='modal_opened'?(a.opened?'✅':'❌'):a.type==='row_action'?'👁️':a.type==='filter_click'?'🔍':'⚠️';
console.log(` ${ai} ${a.type}: ${a.trigger||''} ${a.modal_title||''}${a.error?' [error: '+a.error+']':''}`);
}
for(const e of s.consoleErrors.slice(0,3)) console.log(`${e.substring(0,120)}`);
}
console.log(`=============================`);
console.log(`${results.summary.passed} ⚠️ ${results.summary.warnings}${results.summary.failed}`);
process.exit(results.summary.failed>0?1:0);
}
run().catch(e => { console.error(e); process.exit(1); });