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
222 lines
9.6 KiB
JavaScript
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); });
|