import { test, expect } from '@playwright/test'; /** * Chatbot E2E Test Suite * * This suite tests the AI chatbot functionality including: * - Opening and closing the chat window * - Message flow (sending and receiving) * - Error handling and validation * - UI interactions (clear chat, suggested prompts) * - Accessibility features * * Note: These tests use mocked API responses for reliability. * Integration tests with the real API should be run separately. */ /** * Chatbot UI Tests * * Verify that the chatbot UI renders correctly and interactions work. */ test.describe('Chatbot UI', () => { test.beforeEach(async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); }); test('Chatbot floating button is visible', async ({ page }) => { // Verify the floating chat button exists const chatButton = page.locator('button[aria-label="Open chat"]'); await expect(chatButton).toBeVisible(); }); test('Chatbot opens when floating button is clicked', async ({ page }) => { // Click the chat button const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify chat window opens const chatWindow = page.locator('.fixed.z-50.bg-zinc-900'); await expect(chatWindow).toBeVisible(); // Verify chat header is visible const chatHeader = page.locator('text=AI Assistant'); await expect(chatHeader).toBeVisible(); }); test('Chatbot closes when close button is clicked', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Wait for chat window to be visible const chatWindow = page.locator('.fixed.z-50.bg-zinc-900'); await expect(chatWindow).toBeVisible(); // Click close button const closeButton = page.locator('button[aria-label="Close chat"]'); await closeButton.click(); // Wait for animation and verify chat is closed await page.waitForTimeout(300); // Floating button should be visible again await expect(chatButton).toBeVisible(); }); test('Chatbot shows welcome message when empty', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify welcome message const welcomeHeading = page.locator("text=Hi, I'm Damjan's AI Assistant"); await expect(welcomeHeading).toBeVisible(); // Verify help text const helpText = page.locator('text=I can help you learn about'); await expect(helpText).toBeVisible(); }); test('Chatbot shows suggested prompts when empty', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify "Try asking:" label const tryAskingLabel = page.locator('text=Try asking:'); await expect(tryAskingLabel).toBeVisible(); // Verify suggested prompts are visible const suggestedPrompts = page.locator('button:has-text("What services do you offer?")'); await expect(suggestedPrompts).toBeVisible(); const experiencePrompt = page.locator('button:has-text("Tell me about your experience")'); await expect(experiencePrompt).toBeVisible(); const techPrompt = page.locator('button:has-text("What technologies do you work with?")'); await expect(techPrompt).toBeVisible(); }); test('Chat input field is present and functional', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify input field const inputField = page.locator('input[aria-label="Chat message input"]'); await expect(inputField).toBeVisible(); await expect(inputField).toBeEnabled(); // Verify placeholder await expect(inputField).toHaveAttribute('placeholder', 'Type a message...'); // Type in input await inputField.fill('Test message'); await expect(inputField).toHaveValue('Test message'); }); test('Send button is disabled when input is empty', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify send button is disabled with empty input const sendButton = page.locator('button[aria-label="Send message"]'); await expect(sendButton).toBeDisabled(); // Type in input const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); // Verify send button is now enabled await expect(sendButton).toBeEnabled(); }); test('Privacy note is displayed', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify privacy note const privacyNote = page.locator('text=Messages may be stored to improve service'); await expect(privacyNote).toBeVisible(); }); }); /** * Chatbot Message Flow Tests * * Verify that sending and receiving messages works correctly. * These tests mock API responses for reliability. */ test.describe('Chatbot Message Flow', () => { test.beforeEach(async ({ page }) => { // Mock the chat API endpoint await page.route('**/api/chat', async (route) => { const request = route.request(); const method = request.method(); if (method === 'POST') { // Simulate streaming response for POST const encoder = new TextEncoder(); const chunks = [ 'data: {"content":"Hello","sessionId":"test-session"}\n\n', 'data: {"content":"! How can I","sessionId":"test-session"}\n\n', 'data: {"content":" help you today?","sessionId":"test-session"}\n\n', 'data: {"done":true,"sessionId":"test-session"}\n\n', ]; await route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: chunks.join(''), }); } else if (method === 'GET') { // Return empty history for GET await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); }); test('Sending a message shows user message in chat', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Type and send message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Hello, how are you?'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Verify user message appears in chat const userMessage = page.locator('text=Hello, how are you?'); await expect(userMessage).toBeVisible(); // Input should be cleared after sending await expect(inputField).toHaveValue(''); }); test('Sending a message triggers API response', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Type and send message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Hello'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Wait for assistant response const assistantMessage = page.locator('text=Hello! How can I help you today?'); await expect(assistantMessage).toBeVisible({ timeout: 5000 }); }); test('Clicking suggested prompt sends message', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Click suggested prompt const suggestedPrompt = page.locator('button:has-text("What services do you offer?")'); await suggestedPrompt.click(); // Verify user message appears const userMessage = page.locator('text=What services do you offer?'); await expect(userMessage).toBeVisible(); }); test('Enter key submits the message', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Type message and press Enter const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test enter submission'); await inputField.press('Enter'); // Verify user message appears const userMessage = page.locator('text=Test enter submission'); await expect(userMessage).toBeVisible(); }); test('Input is disabled during message sending', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Type and send message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Input should be disabled during loading await expect(inputField).toBeDisabled(); // Wait for response to complete await page.waitForTimeout(500); }); }); /** * Chatbot Error Handling Tests * * Verify that errors are handled gracefully. */ test.describe('Chatbot Error Handling', () => { test('Shows error message when API fails', async ({ page }) => { // Mock API to return error await page.route('**/api/chat', async (route) => { const method = route.request().method(); if (method === 'POST') { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Internal server error' }), }); } else if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Send message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Wait for error message const errorContainer = page.locator('.bg-red-900\\/20'); await expect(errorContainer).toBeVisible({ timeout: 5000 }); }); test('Error message can be dismissed', async ({ page }) => { // Mock API to return error await page.route('**/api/chat', async (route) => { const method = route.request().method(); if (method === 'POST') { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Test error' }), }); } else if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Send message to trigger error const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Wait for error and dismiss it const errorContainer = page.locator('.bg-red-900\\/20'); await expect(errorContainer).toBeVisible({ timeout: 5000 }); // Click dismiss button const dismissButton = page.locator('text=Dismiss'); await dismissButton.click(); // Error should be hidden await expect(errorContainer).not.toBeVisible(); }); test('Handles streaming error gracefully', async ({ page }) => { // Mock API to return error in stream await page.route('**/api/chat', async (route) => { const method = route.request().method(); if (method === 'POST') { await route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: 'data: {"error":"Stream error occurred"}\n\n', }); } else if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Send message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Error should be shown const errorContainer = page.locator('.bg-red-900\\/20'); await expect(errorContainer).toBeVisible({ timeout: 5000 }); }); }); /** * Chatbot Clear Chat Tests * * Verify that clearing chat works correctly. */ test.describe('Chatbot Clear Chat', () => { test.beforeEach(async ({ page }) => { // Mock the chat API endpoint await page.route('**/api/chat', async (route) => { const method = route.request().method(); if (method === 'POST') { await route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: 'data: {"content":"Test response","sessionId":"test-session"}\n\ndata: {"done":true,"sessionId":"test-session"}\n\n', }); } else if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); }); test('Clear chat button appears after sending message', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Clear button should not be visible initially (no messages) const clearButton = page.locator('button[aria-label="Clear chat"]'); await expect(clearButton).not.toBeVisible(); // Send a message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Wait for message to appear await page.waitForTimeout(500); // Clear button should now be visible await expect(clearButton).toBeVisible(); }); test('Clicking clear chat removes all messages', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Send a message const inputField = page.locator('input[aria-label="Chat message input"]'); await inputField.fill('Test message'); const sendButton = page.locator('button[aria-label="Send message"]'); await sendButton.click(); // Wait for messages await page.waitForTimeout(1000); // Click clear button const clearButton = page.locator('button[aria-label="Clear chat"]'); await clearButton.click(); // Verify messages are cleared and welcome message returns const welcomeHeading = page.locator("text=Hi, I'm Damjan's AI Assistant"); await expect(welcomeHeading).toBeVisible(); // Clear button should be hidden again await expect(clearButton).not.toBeVisible(); }); }); /** * Chatbot Accessibility Tests * * Verify that the chatbot is accessible. */ test.describe('Chatbot Accessibility', () => { test.beforeEach(async ({ page }) => { // Mock the chat API await page.route('**/api/chat', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); }); test('Chatbot floating button has aria-label', async ({ page }) => { const chatButton = page.locator('button[aria-label="Open chat"]'); await expect(chatButton).toBeVisible(); await expect(chatButton).toHaveAttribute('aria-label', 'Open chat'); }); test('Close button has aria-label', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); const closeButton = page.locator('button[aria-label="Close chat"]'); await expect(closeButton).toBeVisible(); await expect(closeButton).toHaveAttribute('aria-label', 'Close chat'); }); test('Input field has aria-label', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); const inputField = page.locator('input[aria-label="Chat message input"]'); await expect(inputField).toBeVisible(); await expect(inputField).toHaveAttribute('aria-label', 'Chat message input'); }); test('Send button has aria-label', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); const sendButton = page.locator('button[aria-label="Send message"]'); await expect(sendButton).toBeVisible(); await expect(sendButton).toHaveAttribute('aria-label', 'Send message'); }); test('Input field receives focus when chat opens', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Wait for focus to be set await page.waitForTimeout(200); // Verify input has focus const inputField = page.locator('input[aria-label="Chat message input"]'); await expect(inputField).toBeFocused(); }); }); /** * Chatbot Mobile Tests * * Verify that the chatbot works correctly on mobile devices. */ test.describe('Chatbot Mobile', () => { test.use({ viewport: { width: 375, height: 812 } }); test.beforeEach(async ({ page }) => { // Mock the chat API await page.route('**/api/chat', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'test-session' }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); }); test('Chatbot floating button is visible on mobile', async ({ page }) => { const chatButton = page.locator('button[aria-label="Open chat"]'); await expect(chatButton).toBeVisible(); }); test('Chatbot opens correctly on mobile', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify chat window is visible const chatWindow = page.locator('.fixed.z-50.bg-zinc-900'); await expect(chatWindow).toBeVisible(); // Verify chat is usable const inputField = page.locator('input[aria-label="Chat message input"]'); await expect(inputField).toBeVisible(); }); test('Chat window does not overflow on mobile', async ({ page }) => { // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Get chat window dimensions const chatWindow = page.locator('.fixed.z-50.bg-zinc-900'); const box = await chatWindow.boundingBox(); expect(box).not.toBeNull(); if (box) { // Chat window should fit within viewport expect(box.x).toBeGreaterThanOrEqual(0); expect(box.y).toBeGreaterThanOrEqual(0); expect(box.x + box.width).toBeLessThanOrEqual(375 + 1); // Allow 1px tolerance } }); }); /** * Chatbot Chat History Tests * * Verify that chat history is loaded correctly. */ test.describe('Chatbot Chat History', () => { test('Loads chat history when opening', async ({ page }) => { // Mock API with existing history await page.route('**/api/chat', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [ { id: '1', role: 'user', content: 'Previous question', created_at: new Date().toISOString() }, { id: '2', role: 'assistant', content: 'Previous answer', created_at: new Date().toISOString() }, ], sessionId: 'existing-session', }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify previous messages are loaded await page.waitForTimeout(500); const previousQuestion = page.locator('text=Previous question'); await expect(previousQuestion).toBeVisible(); const previousAnswer = page.locator('text=Previous answer'); await expect(previousAnswer).toBeVisible(); }); test('Shows welcome message when no history exists', async ({ page }) => { // Mock API with empty history await page.route('**/api/chat', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [], sessionId: 'new-session', }), }); } }); await page.goto('/de', { waitUntil: 'networkidle', }); // Open chat const chatButton = page.locator('button[aria-label="Open chat"]'); await chatButton.click(); // Verify welcome message is shown const welcomeHeading = page.locator("text=Hi, I'm Damjan's AI Assistant"); await expect(welcomeHeading).toBeVisible(); }); });