#!/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); });