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
This commit is contained in:
221
tests/scripts/admin-panel-deep-test.js
Normal file
221
tests/scripts/admin-panel-deep-test.js
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/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); });
|
||||
Reference in New Issue
Block a user