semillero-special-hotel/prisma/test-phase6-ui.js

344 lines
11 KiB
JavaScript

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();