const { spawn } = require('child_process'); const { PrismaClient } = require('@prisma/client'); const { PrismaPg } = require('@prisma/adapter-pg'); const { Pool } = require('pg'); const puppeteer = require('puppeteer'); const fs = require('fs'); const path = require('path'); require('dotenv').config(); const PORT = 3016; const BASE_URL = `http://localhost:${PORT}`; const SCREENSHOT_DIR = path.join(__dirname, 'screenshots-phase6'); let nextProcess; let prisma; let pool; let browser; // Ensure screenshot dir exists if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } function getPrisma() { if (!prisma) { const dbUrl = new URL(process.env.DATABASE_URL); pool = new Pool({ host: dbUrl.hostname, port: dbUrl.port ? parseInt(dbUrl.port) : 5432, user: decodeURIComponent(dbUrl.username), password: decodeURIComponent(dbUrl.password), database: dbUrl.pathname.substring(1).split('?')[0], ssl: false }); const adapter = new PrismaPg(pool); prisma = new PrismaClient({ adapter }); } return prisma; } async function runAsAdmin(queryFn) { const db = getPrisma(); return db.$transaction(async (tx) => { await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = 'admin';`); return queryFn(tx); }); } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } let passed = 0; let failed = 0; function assert(condition, message) { if (condition) { console.log(` ✓ PASS: ${message}`); passed++; } else { console.error(` ✗ FAIL: ${message}`); failed++; } } async function cleanupDb() { console.log("Cleaning up database records for Phase 6 tests..."); await runAsAdmin(async (tx) => { // Delete settlements created during E2E tests await tx.settlement.deleteMany({ where: { period: '2026-88' } }); // Delete goals created during E2E tests await tx.goal.deleteMany({ where: { period: '2026-88' } }); // Delete sales results created during E2E tests await tx.salesResult.deleteMany({ where: { period: '2026-88' } }); }); } async function main() { try { await cleanupDb(); // 1. Fetch seeded users & plans console.log("Fetching seeded users..."); const { adminUser, colaboradorMde, plan } = await runAsAdmin(async (tx) => { const users = await tx.user.findMany({ include: { hotel: true } }); const adminUser = users.find(u => u.username === 'admin'); const colaboradorMde = users.find(u => u.username === 'colaborador_mde'); let p = await tx.compensationPlan.findFirst({ where: { status: 'ACTIVE' } }); if (!p) { p = await tx.compensationPlan.create({ data: { name: 'Test Active Plan', code: 'TEST-ACT-PLAN', validityStart: new Date(), type: 'PERCENTAGE', status: 'ACTIVE', createdBy: adminUser.id } }); } return { adminUser, colaboradorMde, plan: p }; }); // 2. Seed settlement history data for 2026-88 console.log("Seeding test settlement history..."); await runAsAdmin(async (tx) => { await tx.goal.create({ data: { targetType: 'INDIVIDUAL', targetId: colaboradorMde.id, period: '2026-88', amount: 10000.00 } }); await tx.salesResult.create({ data: { source: 'EXCEL', hotelId: colaboradorMde.hotelId, userId: colaboradorMde.id, period: '2026-88', amount: 11000.00, salesCount: 8, idempotencyKey: 'test-key-p6-e2e-1', uploadedBy: adminUser.id, status: 'PROCESSED' } }); await tx.settlement.create({ data: { period: '2026-88', planId: plan.id, userId: colaboradorMde.id, salesAmount: 11000.00, goalAmount: 10000.00, achievementPercentage: 1.10, calculatedCommission: 1500.00, calculatedBonus: 0, adjustmentAmount: 0, totalPayout: 1500.00, status: 'APPROVED', aiAudited: true, aiAuditNotes: { es: 'Nota de auditoría en Español: Todo correcto.', en: 'Audit note in English: All correct.' } } }); }); // 3. Start Next.js production server on port 3016 console.log(`Starting Next.js production server on port ${PORT}...`); nextProcess = spawn('node', ['node_modules/next/dist/bin/next', 'start', '--port', String(PORT)], { env: { ...process.env, PORT: String(PORT), IS_E2E_TEST: 'true' } }); nextProcess.stdout.on('data', (data) => { console.log(`[Next.js] ${data.toString().trim()}`); }); nextProcess.stderr.on('data', (data) => { console.error(`[Next.js ERR] ${data.toString().trim()}`); }); // Wait for server to start up let serverReady = false; for (let i = 0; i < 15; i++) { await sleep(2000); try { await fetch(`${BASE_URL}/api/auth/me`); serverReady = true; break; } catch (e) { // Server not ready yet } } if (!serverReady) { console.error("Error: Next.js server failed to start within timeout."); await cleanup(); process.exit(1); } console.log("Next.js server is ready!"); // 4. Launch Puppeteer browser console.log("Launching Puppeteer browser..."); browser = await puppeteer.launch({ headless: 'shell', args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); // --- TEST 1: COLABORADOR HISTORY --- console.log("\n[Test 1] Logging in as Collaborator and checking history dashboard..."); await page.goto(`${BASE_URL}/login`); await page.type('#username', 'colaborador_mde'); await page.type('#password', 'password123'); await page.click('button[type="submit"]'); await page.waitForNavigation({ waitUntil: 'networkidle0' }); // Verify redirected to /plans or check page header await page.goto(`${BASE_URL}/history`); await page.waitForSelector('[data-settlement-id]'); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '01_history_loaded.png') }); console.log("Screenshot saved: 01_history_loaded.png"); // Verify details const textContent = await page.evaluate(() => document.body.innerText); assert(textContent.includes('2026-88'), "History page displays the correct settlement period."); assert(textContent.includes(plan.code), "History page displays the plan code."); // Toggle notes console.log("Toggling AI audit notes..."); await page.click('[id^="btn-toggle-notes-"]'); await sleep(1000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '02_history_notes_expanded.png') }); console.log("Screenshot saved: 02_history_notes_expanded.png"); const notesText = await page.evaluate(() => document.body.innerText); assert( notesText.includes('Nota de auditoría en Español: Todo correcto.'), "AI Audit Notes display in Spanish (default locale)" ); // Trigger language switch console.log("Switching language to English..."); await page.select('#language-selector', 'en'); await sleep(1000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '03_history_english.png') }); console.log("Screenshot saved: 03_history_english.png"); const englishText = await page.evaluate(() => document.body.innerText); assert( englishText.includes('Audit note in English: All correct.'), "AI Audit Notes successfully localized to English after switcher trigger" ); // Logout console.log("Logging out..."); await page.click('button:last-of-type'); // Logout button await page.waitForNavigation({ waitUntil: 'networkidle0' }); // --- TEST 2: EXECUTIVE DASHBOARD --- console.log("\n[Test 2] Logging in as Admin and checking Executive Dashboard..."); await page.goto(`${BASE_URL}/login`); await page.type('#username', 'admin'); await page.type('#password', 'password123'); await page.click('button[type="submit"]'); await page.waitForNavigation({ waitUntil: 'networkidle0' }); await page.goto(`${BASE_URL}/dashboard`); await page.waitForSelector('#dashboard-main-view'); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '04_dashboard_loaded.png') }); console.log("Screenshot saved: 04_dashboard_loaded.png"); const dashContent = await page.evaluate(() => document.body.innerText); assert(dashContent.includes('Tablero de Análisis de Compensación') || dashContent.includes('Compensation Analytics Dashboard'), "Dashboard page title loaded successfully."); // Check if charts are mounted (recharts responsiveness renders SVG) const chartSvgExists = await page.evaluate(() => { return document.querySelector('.recharts-responsive-container') !== null; }); assert(chartSvgExists, "Executive dashboard renders Recharts visualizations."); // --- TEST 3: SYSTEM AUDIT LOGS --- console.log("\n[Test 3] Navigating to System Audit Logs..."); await page.goto(`${BASE_URL}/admin/audit-logs`); await page.waitForSelector('#audit-logs-main-view'); await page.waitForSelector('[data-audit-id]'); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '05_audit_logs_loaded.png') }); console.log("Screenshot saved: 05_audit_logs_loaded.png"); const auditContent = await page.evaluate(() => document.body.innerText); assert(auditContent.includes('Registros de Auditoría del Sistema') || auditContent.includes('System Audit Logs'), "Audit Logs title loaded successfully."); // Click a row to see JSON diff console.log("Clicking audit log row..."); await page.click('[data-audit-id]'); await sleep(1000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, '06_audit_logs_diff.png') }); console.log("Screenshot saved: 06_audit_logs_diff.png"); const diffPanelText = await page.evaluate(() => document.body.innerText); console.log("DEBUG: diffPanelText is:", diffPanelText); const lowerText = diffPanelText.toLowerCase(); assert(lowerText.includes('anterior') || lowerText.includes('previous') || lowerText.includes('nuevo') || lowerText.includes('new'), "Selecting log entry displays side-by-side JSON diff details panel."); console.log(`\n=== UI E2E TEST RUN COMPLETE: ${passed} PASSED, ${failed} FAILED ===`); } catch (err) { console.error("UI Test script failed:", err); failed++; } finally { await cleanup(); if (failed > 0) { process.exit(1); } else { process.exit(0); } } } // Helper to look up compiled class names or selectors in module.css (or hardcode/approximate them since they start with page_*) function stylesClass(className) { // Since CSS module classes are hashed, we can use selectors containing the class name return className; } async function cleanup() { console.log("Cleaning up resources..."); try { await cleanupDb(); } catch (err) { console.error("Error during cleanupDb:", err); } if (browser) { await browser.close(); } if (nextProcess) { nextProcess.kill('SIGINT'); } if (pool) { await pool.end(); } } main();