fc1ca90b38
overflow: hidden on .chat-row made min-height resolve to 0 (CSS Flexbox §4.5), so the flex column container crushed rows instead of scrolling. Also fix all test selectors (.chat-agent→.chat-row-bot .chat-msg) and add bubble dimension assertions.
401 lines
17 KiB
JavaScript
401 lines
17 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('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);
|
|
});
|