376 lines
15 KiB
JavaScript
376 lines
15 KiB
JavaScript
|
|
#!/usr/bin/env bun
|
||
|
|
/**
|
||
|
|
* CV ASSISTANT MASCOT TEST
|
||
|
|
* =========================
|
||
|
|
* Tests the AI chat widget (mascot) functionality
|
||
|
|
* - Widget visibility and toggle
|
||
|
|
* - Help popup display and dismiss
|
||
|
|
* - Suggested question chips
|
||
|
|
* - Text input and form submission
|
||
|
|
* - Chat messages rendering
|
||
|
|
* - Session persistence
|
||
|
|
* - Language awareness
|
||
|
|
* - Responsive behavior
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { chromium } from 'playwright';
|
||
|
|
|
||
|
|
const URL = "http://localhost:1999";
|
||
|
|
const CHAT_TIMEOUT = 15000; // Agent responses can take a few seconds
|
||
|
|
|
||
|
|
async function testChatMascot() {
|
||
|
|
console.log('🤖 CV ASSISTANT MASCOT TEST\n');
|
||
|
|
console.log('='.repeat(70));
|
||
|
|
|
||
|
|
const browser = await chromium.launch({ headless: false });
|
||
|
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
||
|
|
|
||
|
|
const errors = [];
|
||
|
|
const testResults = [];
|
||
|
|
let passed = 0;
|
||
|
|
let failed = 0;
|
||
|
|
|
||
|
|
function record(name, success, detail = '') {
|
||
|
|
testResults.push({ name, success, detail });
|
||
|
|
if (success) {
|
||
|
|
passed++;
|
||
|
|
console.log(` ✅ ${name}${detail ? ' — ' + detail : ''}`);
|
||
|
|
} else {
|
||
|
|
failed++;
|
||
|
|
console.log(` ❌ ${name}${detail ? ' — ' + detail : ''}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
page.on('console', msg => {
|
||
|
|
if (msg.type() === 'error') {
|
||
|
|
errors.push(msg.text());
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// SETUP: Load page
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n📋 Loading page...");
|
||
|
|
await page.goto(`${URL}/?lang=en`);
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 1: Mascot button exists and is visible
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣ Mascot Button Presence");
|
||
|
|
|
||
|
|
const btnExists = await page.locator('#chat-toggle-btn').isVisible();
|
||
|
|
record('Mascot toggle button is visible', btnExists);
|
||
|
|
|
||
|
|
const btnIcon = await page.locator('#chat-toggle-btn .chat-icon-open').isVisible();
|
||
|
|
record('Mascot icon (robot) is visible', btnIcon);
|
||
|
|
|
||
|
|
const closeIconHidden = await page.locator('#chat-toggle-btn .chat-icon-close').isHidden();
|
||
|
|
record('Close icon is hidden initially', closeIconHidden);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 2: Chat panel starts closed
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n2️⃣ Initial State");
|
||
|
|
|
||
|
|
const panelHidden = await page.locator('#chat-panel').isHidden();
|
||
|
|
record('Chat panel is hidden initially', panelHidden);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 3: Click mascot opens chat panel
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n3️⃣ Open Chat Panel");
|
||
|
|
|
||
|
|
await page.click('#chat-toggle-btn');
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const panelOpen = await page.locator('#chat-panel').isVisible();
|
||
|
|
record('Chat panel opens on click', panelOpen);
|
||
|
|
|
||
|
|
const hasMascotActive = await page.locator('#chat-toggle-btn.mascot-active').count();
|
||
|
|
record('Button has mascot-active class', hasMascotActive > 0);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 4: Help card is visible on first open
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n4️⃣ Help Card (Onboarding)");
|
||
|
|
|
||
|
|
const helpVisible = await page.locator('#chat-help-card.visible').isVisible();
|
||
|
|
record('Help card is visible on first open', helpVisible);
|
||
|
|
|
||
|
|
const helpText = await page.locator('.chat-help-text').textContent();
|
||
|
|
record('Help text contains assistant description',
|
||
|
|
helpText.includes('CV Assistant') || helpText.includes('Asistente'));
|
||
|
|
|
||
|
|
const dismissBtn = await page.locator('.chat-help-dismiss').isVisible();
|
||
|
|
record('Dismiss button is visible', dismissBtn);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 5: Dismiss help card
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n5️⃣ Dismiss Help Card");
|
||
|
|
|
||
|
|
await page.click('.chat-help-dismiss');
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const helpDismissed = await page.locator('#chat-help-card.visible').count();
|
||
|
|
record('Help card dismissed after click', helpDismissed === 0);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 6: Help button re-toggles help card
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n6️⃣ Re-toggle Help Card");
|
||
|
|
|
||
|
|
await page.click('.chat-help-btn');
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const helpReOpened = await page.locator('#chat-help-card.visible').isVisible();
|
||
|
|
record('Help card re-opens via ? button', helpReOpened);
|
||
|
|
|
||
|
|
// Dismiss again for clean state
|
||
|
|
await page.click('.chat-help-dismiss');
|
||
|
|
await page.waitForTimeout(200);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 7: Initial welcome message exists
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n7️⃣ Welcome Message");
|
||
|
|
|
||
|
|
const welcomeMsg = await page.locator('#chat-messages .chat-agent').first().textContent();
|
||
|
|
record('Welcome message is present',
|
||
|
|
welcomeMsg.includes('Ask me anything') || welcomeMsg.includes('Pregúntame'));
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 8: Suggested question chips exist
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n8️⃣ Suggested Question Chips");
|
||
|
|
|
||
|
|
const chipCount = await page.locator('.chat-chip').count();
|
||
|
|
record('5 suggested question chips exist', chipCount === 5, `found ${chipCount}`);
|
||
|
|
|
||
|
|
const firstChipText = await page.locator('.chat-chip').first().textContent();
|
||
|
|
record('First chip has text', firstChipText.length > 0, firstChipText);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 9: Text input exists and is functional
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n9️⃣ Text Input");
|
||
|
|
|
||
|
|
const inputExists = await page.locator('#chat-input').isVisible();
|
||
|
|
record('Chat input is visible', inputExists);
|
||
|
|
|
||
|
|
const placeholder = await page.locator('#chat-input').getAttribute('placeholder');
|
||
|
|
record('Input has placeholder',
|
||
|
|
placeholder.includes('Ask something') || placeholder.includes('Pregunta'));
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 10: Send button exists
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n🔟 Send Button");
|
||
|
|
|
||
|
|
const sendBtn = await page.locator('.chat-send-btn').isVisible();
|
||
|
|
record('Send button is visible', sendBtn);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 11: Chip click fills input and submits
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣1️⃣ Chip Click → Submit");
|
||
|
|
|
||
|
|
const msgCountBefore = await page.locator('#chat-messages .chat-message').count();
|
||
|
|
|
||
|
|
await page.click('.chat-chip >> text=Go projects');
|
||
|
|
// Wait for the response (agent call takes time)
|
||
|
|
await page.waitForSelector('#chat-messages .chat-user', { timeout: CHAT_TIMEOUT });
|
||
|
|
|
||
|
|
const userMsg = await page.locator('#chat-messages .chat-user').last().textContent();
|
||
|
|
record('User message appears after chip click',
|
||
|
|
userMsg.includes('Go projects'), userMsg.substring(0, 50));
|
||
|
|
|
||
|
|
// Wait for agent response
|
||
|
|
await page.waitForSelector('#chat-messages .chat-agent:nth-child(3)', { timeout: CHAT_TIMEOUT });
|
||
|
|
|
||
|
|
const agentMsg = await page.locator('#chat-messages .chat-agent').last().textContent();
|
||
|
|
record('Agent response appears',
|
||
|
|
agentMsg.length > 20, `${agentMsg.substring(0, 60)}...`);
|
||
|
|
|
||
|
|
record('Agent mentions Go projects',
|
||
|
|
agentMsg.toLowerCase().includes('go') || agentMsg.toLowerCase().includes('immich') || agentMsg.toLowerCase().includes('cmux'));
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 12: Type and submit custom question
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣2️⃣ Type Custom Question");
|
||
|
|
|
||
|
|
const userMsgCountBefore = await page.locator('#chat-messages .chat-user').count();
|
||
|
|
await page.fill('#chat-input', 'How many years of experience?');
|
||
|
|
await page.click('.chat-send-btn');
|
||
|
|
|
||
|
|
// Wait for a new user message to appear
|
||
|
|
await page.waitForFunction(
|
||
|
|
(count) => document.querySelectorAll('#chat-messages .chat-user').length > count,
|
||
|
|
userMsgCountBefore,
|
||
|
|
{ timeout: CHAT_TIMEOUT }
|
||
|
|
);
|
||
|
|
const customUserMsg = await page.locator('#chat-messages .chat-user').last().textContent();
|
||
|
|
record('Custom typed message appears',
|
||
|
|
customUserMsg.includes('years of experience'));
|
||
|
|
|
||
|
|
// Wait for agent response
|
||
|
|
await page.waitForFunction(() => {
|
||
|
|
const msgs = document.querySelectorAll('#chat-messages .chat-agent');
|
||
|
|
return msgs.length >= 3;
|
||
|
|
}, { timeout: CHAT_TIMEOUT });
|
||
|
|
|
||
|
|
const customAgentMsg = await page.locator('#chat-messages .chat-agent').last().textContent();
|
||
|
|
record('Agent responds to custom question',
|
||
|
|
customAgentMsg.length > 10, `${customAgentMsg.substring(0, 60)}...`);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 13: Input clears after submit
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣3️⃣ Input Clear After Submit");
|
||
|
|
|
||
|
|
const inputValueAfter = await page.locator('#chat-input').inputValue();
|
||
|
|
record('Input is cleared after submission', inputValueAfter === '');
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 14: Session ID persisted
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣4️⃣ Session Persistence");
|
||
|
|
|
||
|
|
const sessionId = await page.locator('#chat-session-id').inputValue();
|
||
|
|
record('Session ID is set after response',
|
||
|
|
sessionId.length > 0, sessionId.substring(0, 20) + '...');
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 15: Close and reopen panel
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣5️⃣ Close and Reopen");
|
||
|
|
|
||
|
|
await page.click('#chat-toggle-btn');
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const panelClosedAgain = await page.locator('#chat-panel').isHidden();
|
||
|
|
record('Panel closes on second toggle click', panelClosedAgain);
|
||
|
|
|
||
|
|
await page.click('#chat-toggle-btn');
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const panelReopened = await page.locator('#chat-panel').isVisible();
|
||
|
|
record('Panel reopens on third click', panelReopened);
|
||
|
|
|
||
|
|
// Messages should still be there
|
||
|
|
const msgCountAfterReopen = await page.locator('#chat-messages .chat-message').count();
|
||
|
|
record('Messages preserved after reopen', msgCountAfterReopen >= 3,
|
||
|
|
`${msgCountAfterReopen} messages`);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 16: Chat header content
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣6️⃣ Header Content");
|
||
|
|
|
||
|
|
const headerText = await page.locator('.chat-header span').textContent();
|
||
|
|
record('Header shows "CV Assistant"',
|
||
|
|
headerText.includes('CV Assistant') || headerText.includes('Asistente'));
|
||
|
|
|
||
|
|
const helpBtnExists = await page.locator('.chat-help-btn').isVisible();
|
||
|
|
record('Help button (?) in header', helpBtnExists);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 17: Spanish language version
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣7️⃣ Spanish Language");
|
||
|
|
|
||
|
|
// Close chat first
|
||
|
|
await page.click('#chat-toggle-btn');
|
||
|
|
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 esHeaderText = await page.locator('.chat-header span').textContent();
|
||
|
|
record('Spanish header shows "Asistente del CV"',
|
||
|
|
esHeaderText.includes('Asistente del CV'));
|
||
|
|
|
||
|
|
const esChipText = await page.locator('.chat-chip').first().textContent();
|
||
|
|
record('Spanish chips are in Spanish',
|
||
|
|
esChipText.includes('Go') || esChipText.includes('Proyectos'));
|
||
|
|
|
||
|
|
const esWelcome = await page.locator('#chat-messages .chat-agent').first().textContent();
|
||
|
|
record('Spanish welcome message',
|
||
|
|
esWelcome.includes('Pregúntame') || esWelcome.includes('Hola'));
|
||
|
|
|
||
|
|
const esPlaceholder = await page.locator('#chat-input').getAttribute('placeholder');
|
||
|
|
record('Spanish placeholder',
|
||
|
|
esPlaceholder.includes('Pregunta'));
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 18: Empty message handling
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣8️⃣ Empty Message Handling");
|
||
|
|
|
||
|
|
const msgCountBeforeEmpty = await page.locator('#chat-messages .chat-message').count();
|
||
|
|
await page.fill('#chat-input', '');
|
||
|
|
await page.click('.chat-send-btn');
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
const msgCountAfterEmpty = await page.locator('#chat-messages .chat-message').count();
|
||
|
|
// Should show error or stay the same
|
||
|
|
record('Empty message handled gracefully', msgCountAfterEmpty >= msgCountBeforeEmpty);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 19: No console errors
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n1️⃣9️⃣ Console Errors");
|
||
|
|
|
||
|
|
const chatErrors = errors.filter(e =>
|
||
|
|
e.includes('chat') || e.includes('htmx') || e.includes('hyperscript'));
|
||
|
|
record('No chat-related console errors', chatErrors.length === 0,
|
||
|
|
chatErrors.length > 0 ? chatErrors.join(', ') : 'clean');
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// TEST 20: Chat panel CSS positioning
|
||
|
|
// ========================================================================
|
||
|
|
console.log("\n2️⃣0️⃣ CSS Positioning");
|
||
|
|
|
||
|
|
const btnPos = await page.locator('#chat-toggle-btn').boundingBox();
|
||
|
|
record('Button is on the left half of screen',
|
||
|
|
btnPos && btnPos.x < 200, `x=${btnPos?.x}`);
|
||
|
|
|
||
|
|
const panelPos = await page.locator('#chat-panel').boundingBox();
|
||
|
|
record('Panel is on the left side',
|
||
|
|
panelPos && panelPos.x < 200, `x=${panelPos?.x}`);
|
||
|
|
|
||
|
|
// ========================================================================
|
||
|
|
// SUMMARY
|
||
|
|
// ========================================================================
|
||
|
|
console.log('\n' + '='.repeat(70));
|
||
|
|
console.log(`\n📊 RESULTS: ${passed} passed, ${failed} failed (${testResults.length} total)\n`);
|
||
|
|
|
||
|
|
if (failed > 0) {
|
||
|
|
console.log('❌ FAILED TESTS:');
|
||
|
|
testResults.filter(t => !t.success).forEach(t => {
|
||
|
|
console.log(` • ${t.name}${t.detail ? ' — ' + t.detail : ''}`);
|
||
|
|
});
|
||
|
|
console.log('');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (errors.length > 0) {
|
||
|
|
console.log(`⚠️ Console errors: ${errors.length}`);
|
||
|
|
errors.forEach(e => console.log(` • ${e}`));
|
||
|
|
}
|
||
|
|
|
||
|
|
await browser.close();
|
||
|
|
|
||
|
|
console.log(failed === 0 ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED');
|
||
|
|
process.exit(failed > 0 ? 1 : 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
testChatMascot().catch(err => {
|
||
|
|
console.error('💥 Test crashed:', err.message);
|
||
|
|
process.exit(1);
|
||
|
|
});
|