Files
Portfolio/tests/e2e/chatbot.spec.ts
T
damjan_savicandClaude Opus 4.5 ddcd08ac06 auto-claude: subtask-7-3 - Create chatbot E2E tests for message flow and error handling
Add comprehensive E2E tests for the chatbot component including:
- Chatbot UI tests (open/close, welcome message, suggested prompts)
- Message flow tests (sending messages, receiving responses, enter key submission)
- Error handling tests (API failures, streaming errors, dismissible errors)
- Clear chat functionality tests
- Accessibility tests (ARIA labels, focus management)
- Mobile responsiveness tests
- Chat history loading tests

Tests use mocked API responses for reliability and isolation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 01:49:56 +01:00

710 lines
22 KiB
TypeScript

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();
});
});