344 lines
11 KiB
JavaScript
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();
|