Files
cv-site/tests/mjs/83-chat-mascot.test.mjs
T
juanatsap 465af719e9 test: rewrite mascot tests — 39 assertions, Gemini + navigation links
Complete rewrite matching current architecture:
- Button position (right side x=1838)
- Panel toggle (open/close/reopen)
- Help modal (5 accordion sections, 18 clickable questions)
- Chip click → Gemini response with nav links
- Typed question → certifications response
- Cross-section: Go (finds projects + skills), Java (finds Insa)
- Company listing (Olympic, SAP, Insa)
- Navigation link presence and anchor hrefs
- Spanish language (header, chips, welcome, response in Spanish)
- Response time: 2.0s (under 10s threshold)
- Session persistence, input clear, console errors

37/39 pass (2 nav-click tests fail in headless mode — works in browser)
2026-04-08 17:17:36 +01:00

363 lines
15 KiB
JavaScript

#!/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('.chat-help-btn').isVisible());
await page.click('.chat-help-btn');
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-agent').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-message').count();
await page.locator('.chat-chip').first().click();
// Wait for response
await page.waitForFunction(
(before) => document.querySelectorAll('#chat-messages .chat-message').length > before + 1,
msgsBefore,
{ timeout: CHAT_TIMEOUT }
);
const userMsg = await page.locator('#chat-messages .chat-user').last().textContent();
record('User message appears', userMsg.length > 5, userMsg.substring(0, 40));
const agentMsg = await page.locator('#chat-messages .chat-agent').last().textContent();
record('Agent response appears', agentMsg.length > 30, `${agentMsg.substring(0, 50)}...`);
// ======================================================================
// 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-user').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-user').length > count,
msgsBeforeType,
{ timeout: CHAT_TIMEOUT }
);
const typedMsg = await page.locator('#chat-messages .chat-user').last().textContent();
record('Typed message appears', typedMsg.includes('certifications'));
const certResponse = await page.locator('#chat-messages .chat-agent').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-agent').count();
await page.waitForFunction(
(c) => document.querySelectorAll('#chat-messages .chat-agent').length > c,
agentsBefore11,
{ timeout: CHAT_TIMEOUT }
);
const goResp = (await page.locator('#chat-messages .chat-agent').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-agent').count();
await page.waitForFunction(
(c) => document.querySelectorAll('#chat-messages .chat-agent').length > c,
agentsBefore12,
{ timeout: CHAT_TIMEOUT }
);
const javaResp = (await page.locator('#chat-messages .chat-agent').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-agent').count();
await page.waitForFunction(
(c) => document.querySelectorAll('#chat-messages .chat-agent').length > c,
agentsBefore13,
{ timeout: CHAT_TIMEOUT }
);
const compResp = (await page.locator('#chat-messages .chat-agent').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-agent').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-agent').count();
await page.waitForFunction(
(c) => document.querySelectorAll('#chat-messages .chat-agent').length > c,
agentsBefore16,
{ timeout: CHAT_TIMEOUT }
);
const esResp = await page.locator('#chat-messages .chat-agent').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-agent').count();
await page.waitForFunction(
(c) => document.querySelectorAll('#chat-messages .chat-agent').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);
});