#!/usr/bin/env bun /** * CV ASSISTANT MASCOT TEST * ========================= * Tests the AI chat mascot: UI, chips, navigation links, help modal, * Gemini responses, cross-section intelligence, and bilingual support. */ import { chromium } from 'playwright'; const URL = "http://localhost:1999"; const CHAT_TIMEOUT = 20000; async function testChatMascot() { console.log('🤖 CV ASSISTANT MASCOT TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); const errors = []; let passed = 0; let failed = 0; const results = []; function record(name, ok, detail = '') { results.push({ name, ok }); ok ? passed++ : failed++; console.log(` ${ok ? '✅' : '❌'} ${name}${detail ? ' — ' + detail : ''}`); } page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); // ====================================================================== console.log("\n📋 Loading page..."); await page.goto(`${URL}/?lang=en`); await page.waitForTimeout(2000); // ====================================================================== // 1. BUTTON // ====================================================================== console.log("\n1️⃣ Mascot Button"); record('Button visible', await page.locator('#chat-toggle-btn').isVisible()); record('Robot icon visible', await page.locator('.chat-icon-open').isVisible()); record('Close icon hidden', await page.locator('.chat-icon-close').isHidden()); const btnBox = await page.locator('#chat-toggle-btn').boundingBox(); record('Button on right side', btnBox && btnBox.x > 1800, `x=${btnBox?.x}`); // ====================================================================== // 2. PANEL TOGGLE // ====================================================================== console.log("\n2️⃣ Panel Toggle"); record('Panel hidden initially', await page.locator('#chat-panel').isHidden()); await page.click('#chat-toggle-btn'); await page.waitForTimeout(500); record('Panel opens on click', await page.locator('#chat-panel.chat-open').isVisible()); record('Button has mascot-active', (await page.locator('#chat-toggle-btn.mascot-active').count()) > 0); await page.click('#chat-toggle-btn'); await page.waitForTimeout(300); record('Panel closes on second click', await page.locator('#chat-panel').isHidden()); await page.click('#chat-toggle-btn'); await page.waitForTimeout(300); // ====================================================================== // 3. HELP MODAL // ====================================================================== console.log("\n3️⃣ Help Modal"); record('Help button (?) visible', await page.locator('button[command="show-modal"][commandfor="chat-help-modal"]').isVisible()); await page.click('button[command="show-modal"][commandfor="chat-help-modal"]'); await page.waitForTimeout(500); const modalOpen = await page.locator('#chat-help-modal').evaluate(el => el.open); record('Help modal opens', modalOpen); const accordionCount = await page.locator('.chat-help-group').count(); record('5 accordion sections', accordionCount === 5, `found ${accordionCount}`); const firstOpen = await page.locator('.chat-help-group[open]').count(); record('First section expanded by default', firstOpen >= 1); const questionCount = await page.locator('.chat-help-q').count(); record('18+ clickable questions', questionCount >= 18, `found ${questionCount}`); // Close modal await page.locator('#chat-help-modal .info-modal-close').click(); await page.waitForTimeout(300); // ====================================================================== // 4. WELCOME MESSAGE // ====================================================================== console.log("\n4️⃣ Welcome Message"); const welcome = await page.locator('#chat-messages .chat-row-bot .chat-msg').first().textContent(); record('Welcome message present', welcome.includes('Ask me anything') || welcome.includes('Pregúntame')); // ====================================================================== // 5. CHIPS // ====================================================================== console.log("\n5️⃣ Suggested Chips"); const chipCount = await page.locator('.chat-chip').count(); record('5 chips exist', chipCount === 5, `found ${chipCount}`); // ====================================================================== // 6. CHIP CLICK → GEMINI RESPONSE // ====================================================================== console.log("\n6️⃣ Chip Click → Response"); const msgsBefore = await page.locator('#chat-messages .chat-row').count(); await page.locator('.chat-chip').first().click(); // Wait for response await page.waitForFunction( (before) => document.querySelectorAll('#chat-messages .chat-row').length > before + 1, msgsBefore, { timeout: CHAT_TIMEOUT } ); const userMsg = await page.locator('#chat-messages .chat-row-user .chat-msg').last().textContent(); record('User message appears', userMsg.length > 5, userMsg.substring(0, 40)); const agentMsg = await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent(); record('Agent response appears', agentMsg.length > 30, `${agentMsg.substring(0, 50)}...`); // ====================================================================== // 6b. BUBBLE RENDERING — user and bot bubbles must have proper dimensions // ====================================================================== console.log("\n6️⃣b Bubble Rendering"); // Scroll to make last user bubble visible await page.locator('#chat-messages .chat-row-user').last().scrollIntoViewIfNeeded(); await page.waitForTimeout(200); const lastUserBubble = page.locator('#chat-messages .chat-row-user .chat-msg').last(); const userBubbleBox = await lastUserBubble.boundingBox(); record('User bubble width > 80px', userBubbleBox && userBubbleBox.width > 80, `${Math.round(userBubbleBox?.width || 0)}px`); record('User bubble height > 15px', userBubbleBox && userBubbleBox.height > 15, `${Math.round(userBubbleBox?.height || 0)}px`); const lastBotBubble = page.locator('#chat-messages .chat-row-bot .chat-msg').last(); await lastBotBubble.scrollIntoViewIfNeeded(); await page.waitForTimeout(200); const botBubbleBox = await lastBotBubble.boundingBox(); record('Bot bubble width > 100px', botBubbleBox && botBubbleBox.width > 100, `${Math.round(botBubbleBox?.width || 0)}px`); record('Bot bubble height > 15px', botBubbleBox && botBubbleBox.height > 15, `${Math.round(botBubbleBox?.height || 0)}px`); // User bubble must be ABOVE bot response — use DOM offsets (scroll-independent) const noOverlap = await page.evaluate(() => { const userRows = document.querySelectorAll('#chat-messages .chat-row-user'); const botRows = document.querySelectorAll('#chat-messages .chat-row-bot'); const lastUser = userRows[userRows.length - 1]; const lastBot = botRows[botRows.length - 1]; if (!lastUser || !lastBot) return { ok: false, detail: 'elements not found' }; const userBottom = lastUser.offsetTop + lastUser.offsetHeight; const botTop = lastBot.offsetTop; return { ok: userBottom <= botTop, detail: 'user ends=' + userBottom + ', bot starts=' + botTop }; }); record('Bubbles don\'t overlap vertically', noOverlap.ok, noOverlap.detail); // ====================================================================== // 7. NAVIGATION LINKS IN RESPONSE // ====================================================================== console.log("\n7️⃣ Navigation Links"); const navLinkCount = await page.locator('#chat-messages .chat-nav-link').count(); record('Response has navigation links', navLinkCount > 0, `found ${navLinkCount}`); if (navLinkCount > 0) { const firstLinkHref = await page.locator('#chat-messages .chat-nav-link').first().getAttribute('href'); record('Links have anchor hrefs', firstLinkHref && firstLinkHref.startsWith('#'), firstLinkHref); } // ====================================================================== // 8. TYPED QUESTION // ====================================================================== console.log("\n8️⃣ Typed Question"); const msgsBeforeType = await page.locator('#chat-messages .chat-row-user .chat-msg').count(); await page.fill('#chat-input', 'What certifications does he have?'); await page.click('.chat-send-btn'); await page.waitForFunction( (count) => document.querySelectorAll('#chat-messages .chat-row-user .chat-msg').length > count, msgsBeforeType, { timeout: CHAT_TIMEOUT } ); const typedMsg = await page.locator('#chat-messages .chat-row-user .chat-msg').last().textContent(); record('Typed message appears', typedMsg.includes('certifications')); const certResponse = await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent(); record('Certifications response', certResponse.toLowerCase().includes('sap') || certResponse.toLowerCase().includes('certif')); // ====================================================================== // 9. INPUT CLEARS // ====================================================================== console.log("\n9️⃣ Input Clear"); const inputVal = await page.locator('#chat-input').inputValue(); record('Input cleared after submit', inputVal === ''); // ====================================================================== // 10. SESSION PERSISTENCE // ====================================================================== console.log("\n🔟 Session"); const sessionId = await page.locator('#chat-session-id').inputValue(); record('Session ID set', sessionId.length > 10, sessionId.substring(0, 20)); // ====================================================================== // 11. CROSS-SECTION INTELLIGENCE (Go) // ====================================================================== console.log("\n1️⃣1️⃣ Intelligence: Go cross-section"); await page.fill('#chat-input', 'What is Juan\'s experience with Go?'); await page.click('.chat-send-btn'); const agentsBefore11 = await page.locator('#chat-messages .chat-row-bot .chat-msg').count(); await page.waitForFunction( (c) => document.querySelectorAll('#chat-messages .chat-row-bot .chat-msg').length > c, agentsBefore11, { timeout: CHAT_TIMEOUT } ); const goResp = (await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent()).toLowerCase(); record('Go: finds projects', goResp.includes('immich') || goResp.includes('cmux')); record('Go: finds skills', goResp.includes('skill') || goResp.includes('proficiency') || goResp.includes('programming')); // ====================================================================== // 12. CROSS-SECTION INTELLIGENCE (Java) // ====================================================================== console.log("\n1️⃣2️⃣ Intelligence: Java cross-section"); await page.fill('#chat-input', 'What Java experience does he have?'); await page.click('.chat-send-btn'); const agentsBefore12 = await page.locator('#chat-messages .chat-row-bot .chat-msg').count(); await page.waitForFunction( (c) => document.querySelectorAll('#chat-messages .chat-row-bot .chat-msg').length > c, agentsBefore12, { timeout: CHAT_TIMEOUT } ); const javaResp = (await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent()).toLowerCase(); record('Java: finds Insa', javaResp.includes('insa')); record('Java: finds multiple companies', javaResp.includes('homeria') || javaResp.includes('webratio') || javaResp.includes('penta')); // ====================================================================== // 13. COMPANIES LIST // ====================================================================== console.log("\n1️⃣3️⃣ Intelligence: Companies"); await page.fill('#chat-input', 'List all companies he worked at'); await page.click('.chat-send-btn'); const agentsBefore13 = await page.locator('#chat-messages .chat-row-bot .chat-msg').count(); await page.waitForFunction( (c) => document.querySelectorAll('#chat-messages .chat-row-bot .chat-msg').length > c, agentsBefore13, { timeout: CHAT_TIMEOUT } ); const compResp = (await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent()).toLowerCase(); record('Lists Olympic', compResp.includes('olympic')); record('Lists SAP', compResp.includes('sap')); record('Lists Insa', compResp.includes('insa')); // ====================================================================== // 14. NAVIGATION LINK CLICK (scroll + highlight) // ====================================================================== console.log("\n1️⃣4️⃣ Navigation Link Click"); const navLinks = page.locator('#chat-messages .chat-nav-link'); const navCount = await navLinks.count(); if (navCount > 0) { await navLinks.first().click(); await page.waitForTimeout(1000); // Panel should close after nav click const panelAfterNav = await page.locator('#chat-panel.chat-open').count(); record('Panel closes after nav click', panelAfterNav === 0); // Check highlight exists somewhere const highlighted = await page.locator('.chat-highlight').count(); record('Target element highlighted', highlighted > 0); // Reopen chat for remaining tests await page.click('#chat-toggle-btn'); await page.waitForTimeout(300); } else { record('Navigation link click (no links)', false, 'no nav links found'); record('Target highlight (skipped)', false); } // ====================================================================== // 15. SPANISH LANGUAGE // ====================================================================== console.log("\n1️⃣5️⃣ Spanish Language"); await page.click('#chat-toggle-btn'); // close await page.waitForTimeout(200); await page.goto(`${URL}/?lang=es`); await page.waitForTimeout(2000); await page.click('#chat-toggle-btn'); await page.waitForTimeout(500); const esHeader = await page.locator('.chat-header span').textContent(); record('Spanish header', esHeader.includes('Asistente')); const esChip = await page.locator('.chat-chip').first().textContent(); record('Spanish chips', esChip.includes('Go') || esChip.includes('Proyectos')); const esWelcome = await page.locator('#chat-messages .chat-row-bot .chat-msg').first().textContent(); record('Spanish welcome', esWelcome.includes('Pregúntame')); // ====================================================================== // 16. SPANISH RESPONSE // ====================================================================== console.log("\n1️⃣6️⃣ Spanish Intelligence"); await page.fill('#chat-input', '¿Cuántos años de experiencia tiene?'); await page.click('.chat-send-btn'); const agentsBefore16 = await page.locator('#chat-messages .chat-row-bot .chat-msg').count(); await page.waitForFunction( (c) => document.querySelectorAll('#chat-messages .chat-row-bot .chat-msg').length > c, agentsBefore16, { timeout: CHAT_TIMEOUT } ); const esResp = await page.locator('#chat-messages .chat-row-bot .chat-msg').last().textContent(); record('Responds in Spanish', esResp.includes('años') || esResp.includes('experiencia')); record('Reports 21 years', esResp.includes('21')); // ====================================================================== // 17. RESPONSE TIME // ====================================================================== console.log("\n1️⃣7️⃣ Response Time"); // Go back to English for timing test await page.click('#chat-toggle-btn'); await page.goto(`${URL}/?lang=en`); await page.waitForTimeout(2000); await page.click('#chat-toggle-btn'); await page.waitForTimeout(300); const startTime = Date.now(); await page.fill('#chat-input', 'How many years of experience?'); await page.click('.chat-send-btn'); const agentsBefore17 = await page.locator('#chat-messages .chat-row-bot .chat-msg').count(); await page.waitForFunction( (c) => document.querySelectorAll('#chat-messages .chat-row-bot .chat-msg').length > c, agentsBefore17, { timeout: CHAT_TIMEOUT } ); const responseTime = Date.now() - startTime; record('Response under 10 seconds', responseTime < 10000, `${(responseTime / 1000).toFixed(1)}s`); // ====================================================================== // 18. ERROR-FREE // ====================================================================== console.log("\n1️⃣8️⃣ Console Errors"); const chatErrors = errors.filter(e => e.includes('chat') || e.includes('htmx')); record('No chat console errors', chatErrors.length === 0, chatErrors.length > 0 ? chatErrors.join('; ') : 'clean'); // ====================================================================== // SUMMARY // ====================================================================== console.log('\n' + '='.repeat(70)); console.log(`\n📊 RESULTS: ${passed} passed, ${failed} failed (${results.length} total)\n`); if (failed > 0) { console.log('❌ FAILED:'); results.filter(r => !r.ok).forEach(r => console.log(` • ${r.name}`)); console.log(''); } await browser.close(); console.log(failed === 0 ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED'); process.exit(failed > 0 ? 1 : 0); } testChatMascot().catch(err => { console.error('💥 Crash:', err.message); process.exit(1); });