Set up comprehensive frontend testing infrastructure

- Install Jest for unit testing with React Testing Library
- Install Playwright for end-to-end testing
- Configure Jest with proper TypeScript support and module mapping
- Create test setup files and utilities for both unit and e2e tests

Components:
* Jest configuration with coverage thresholds
* Playwright configuration with browser automation
* Unit tests for LoginForm, AuthContext, and useSocketIO hook
* E2E tests for authentication, dashboard, and agents workflows
* GitHub Actions workflow for automated testing
* Mock data and API utilities for consistent testing
* Test documentation with best practices

Testing features:
- Unit tests with 70% coverage threshold
- E2E tests with API mocking and user journey testing
- CI/CD integration for automated test runs
- Cross-browser testing support with Playwright
- Authentication system testing end-to-end

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-11 14:06:34 +10:00
parent c6d69695a8
commit aacb45156b
6109 changed files with 777927 additions and 1 deletions

212
frontend/e2e/agents.spec.ts Normal file
View File

@@ -0,0 +1,212 @@
import { test, expect } from '@playwright/test';
test.describe('Agents Management', () => {
test.beforeEach(async ({ page }) => {
// Mock authentication
await page.addInitScript(() => {
localStorage.setItem('token', 'mock-jwt-token');
});
await page.route('**/api/auth/me', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
role: 'admin',
is_active: true,
is_superuser: true,
is_verified: true,
}),
});
});
// Mock agents API
await page.route('**/api/agents', async route => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 'agent-1',
name: 'Walnut CodeLlama',
endpoint: 'http://walnut.local:11434',
model: 'codellama:34b',
specialty: 'kernel_dev',
status: 'online',
current_tasks: 1,
max_concurrent: 2,
agent_type: 'ollama',
last_heartbeat: Date.now(),
},
{
id: 'agent-2',
name: 'Oak Gemma',
endpoint: 'http://oak.local:11434',
model: 'gemma2:27b',
specialty: 'pytorch_dev',
status: 'offline',
current_tasks: 0,
max_concurrent: 2,
agent_type: 'ollama',
last_heartbeat: Date.now() - 300000, // 5 minutes ago
},
]),
});
}
});
});
test('should display agents list', async ({ page }) => {
await page.goto('/agents');
// Check page title
await expect(page.getByRole('heading', { name: /agents/i })).toBeVisible();
// Check agent cards
await expect(page.getByText('Walnut CodeLlama')).toBeVisible();
await expect(page.getByText('Oak Gemma')).toBeVisible();
// Check agent details
await expect(page.getByText('codellama:34b')).toBeVisible();
await expect(page.getByText('gemma2:27b')).toBeVisible();
await expect(page.getByText('kernel_dev')).toBeVisible();
await expect(page.getByText('pytorch_dev')).toBeVisible();
});
test('should show agent status indicators', async ({ page }) => {
await page.goto('/agents');
// Check online status
await expect(page.getByText('online')).toBeVisible();
await expect(page.getByText('offline')).toBeVisible();
});
test('should display agent utilization', async ({ page }) => {
await page.goto('/agents');
// Check task counts
await expect(page.getByText('1/2')).toBeVisible(); // Current/max tasks for agent-1
await expect(page.getByText('0/2')).toBeVisible(); // Current/max tasks for agent-2
});
test('should filter agents by status', async ({ page }) => {
await page.goto('/agents');
// Apply online filter
await page.getByRole('button', { name: /filter/i }).click();
await page.getByRole('option', { name: /online/i }).click();
// Should show only online agents
await expect(page.getByText('Walnut CodeLlama')).toBeVisible();
// Offline agent might be hidden depending on implementation
});
test('should search agents by name', async ({ page }) => {
await page.goto('/agents');
// Search for specific agent
const searchInput = page.getByPlaceholder(/search agents/i);
await searchInput.fill('Walnut');
// Should show filtered results
await expect(page.getByText('Walnut CodeLlama')).toBeVisible();
});
test('should show agent details in modal/popup', async ({ page }) => {
await page.goto('/agents');
// Click on agent to view details
await page.getByText('Walnut CodeLlama').click();
// Check that detail view opens
await expect(page.getByText('http://walnut.local:11434')).toBeVisible();
await expect(page.getByText('codellama:34b')).toBeVisible();
});
test('should handle agent creation for admin users', async ({ page }) => {
await page.goto('/agents');
// Mock agent creation
await page.route('**/api/agents', async route => {
if (route.request().method() === 'POST') {
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
id: 'agent-3',
name: 'New Agent',
endpoint: 'http://localhost:11434',
model: 'llama3.1:8b',
specialty: 'general_ai',
status: 'online',
current_tasks: 0,
max_concurrent: 2,
agent_type: 'ollama',
}),
});
}
});
// Look for add agent button (admin only)
const addButton = page.getByRole('button', { name: /add agent/i });
await expect(addButton).toBeVisible();
// Click add agent
await addButton.click();
// Fill agent form
await page.getByLabel(/name/i).fill('New Agent');
await page.getByLabel(/endpoint/i).fill('http://localhost:11434');
await page.getByLabel(/model/i).fill('llama3.1:8b');
// Submit form
await page.getByRole('button', { name: /create/i }).click();
// Should show success message or new agent in list
await expect(page.getByText('New Agent')).toBeVisible();
});
test('should handle agent health checks', async ({ page }) => {
await page.goto('/agents');
// Mock health check endpoint
await page.route('**/api/agents/agent-1/health', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'healthy',
response_time: 125,
last_check: new Date().toISOString(),
}),
});
});
// Trigger health check
await page.getByRole('button', { name: /check health/i }).first().click();
// Should show health status
await expect(page.getByText(/healthy/i)).toBeVisible();
});
test('should handle agent errors gracefully', async ({ page }) => {
// Mock API error
await page.route('**/api/agents', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ detail: 'Internal server error' }),
});
});
await page.goto('/agents');
// Should show error message
await expect(page.getByText(/error loading agents/i)).toBeVisible();
});
});

175
frontend/e2e/auth.spec.ts Normal file
View File

@@ -0,0 +1,175 @@
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
// Start fresh for each test
await page.goto('/');
});
test('should display login form on unauthenticated access', async ({ page }) => {
// Should redirect to login page
await expect(page).toHaveURL(/.*\/login/);
// Check login form elements
await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible();
await expect(page.getByLabel(/email/i)).toBeVisible();
await expect(page.getByLabel(/password/i)).toBeVisible();
await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible();
});
test('should show validation errors for empty login form', async ({ page }) => {
await page.goto('/login');
// Try to submit empty form
await page.getByRole('button', { name: /sign in/i }).click();
// Check for validation errors
await expect(page.getByText(/email is required/i)).toBeVisible();
await expect(page.getByText(/password is required/i)).toBeVisible();
});
test('should show error for invalid email format', async ({ page }) => {
await page.goto('/login');
// Enter invalid email
await page.getByLabel(/email/i).fill('invalid-email');
await page.getByLabel(/password/i).fill('password123');
await page.getByRole('button', { name: /sign in/i }).click();
// Check for validation error
await expect(page.getByText(/invalid email format/i)).toBeVisible();
});
test('should handle login failure gracefully', async ({ page }) => {
await page.goto('/login');
// Mock failed login response
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ detail: 'Invalid credentials' }),
});
});
// Fill and submit form
await page.getByLabel(/email/i).fill('test@example.com');
await page.getByLabel(/password/i).fill('wrongpassword');
await page.getByRole('button', { name: /sign in/i }).click();
// Check for error message
await expect(page.getByText(/invalid credentials/i)).toBeVisible();
// Should still be on login page
await expect(page).toHaveURL(/.*\/login/);
});
test('should successfully login with valid credentials', async ({ page }) => {
await page.goto('/login');
// Mock successful login response
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
access_token: 'mock-jwt-token',
token_type: 'bearer',
user: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
role: 'user',
is_active: true,
is_superuser: false,
is_verified: true,
},
}),
});
});
// Mock the /me endpoint for authenticated user
await page.route('**/api/auth/me', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
role: 'user',
is_active: true,
is_superuser: false,
is_verified: true,
}),
});
});
// Fill and submit form
await page.getByLabel(/email/i).fill('test@example.com');
await page.getByLabel(/password/i).fill('password123');
await page.getByRole('button', { name: /sign in/i }).click();
// Should redirect to dashboard
await expect(page).toHaveURL(/.*\/dashboard/);
// Check that user is logged in (user menu should be visible)
await expect(page.getByText('Test User')).toBeVisible();
});
test('should toggle password visibility', async ({ page }) => {
await page.goto('/login');
const passwordInput = page.getByLabel(/password/i);
const toggleButton = page.getByRole('button', { name: /toggle password visibility/i });
// Initially password should be hidden
await expect(passwordInput).toHaveAttribute('type', 'password');
// Click toggle to show password
await toggleButton.click();
await expect(passwordInput).toHaveAttribute('type', 'text');
// Click toggle again to hide password
await toggleButton.click();
await expect(passwordInput).toHaveAttribute('type', 'password');
});
test('should logout successfully', async ({ page }) => {
// First login
await page.goto('/login');
// Mock successful login
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
access_token: 'mock-jwt-token',
token_type: 'bearer',
user: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
},
}),
});
});
await page.getByLabel(/email/i).fill('test@example.com');
await page.getByLabel(/password/i).fill('password123');
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for dashboard
await expect(page).toHaveURL(/.*\/dashboard/);
// Click logout
await page.getByRole('button', { name: /logout/i }).click();
// Should redirect to login page
await expect(page).toHaveURL(/.*\/login/);
});
});

View File

@@ -0,0 +1,155 @@
import { test, expect } from '@playwright/test';
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Mock authentication
await page.addInitScript(() => {
localStorage.setItem('token', 'mock-jwt-token');
});
// Mock authenticated user endpoint
await page.route('**/api/auth/me', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
role: 'user',
is_active: true,
is_superuser: false,
is_verified: true,
}),
});
});
// Mock dashboard data endpoints
await page.route('**/api/agents', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 'agent-1',
name: 'Test Agent',
endpoint: 'http://localhost:11434',
model: 'llama3.1:8b',
specialty: 'general_ai',
status: 'online',
current_tasks: 1,
max_concurrent: 2,
},
]),
});
});
await page.route('**/api/tasks', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 'task-1',
title: 'Test Task',
type: 'general_ai',
status: 'pending',
priority: 3,
created_at: new Date().toISOString(),
},
]),
});
});
await page.route('**/api/health', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'operational',
agents: {},
total_agents: 1,
active_tasks: 1,
pending_tasks: 0,
completed_tasks: 5,
}),
});
});
});
test('should display dashboard content when authenticated', async ({ page }) => {
await page.goto('/dashboard');
// Check dashboard elements
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
await expect(page.getByText('Test User')).toBeVisible();
// Check for system status cards
await expect(page.getByText(/agents/i)).toBeVisible();
await expect(page.getByText(/tasks/i)).toBeVisible();
});
test('should display agent status', async ({ page }) => {
await page.goto('/dashboard');
// Wait for agents to load
await expect(page.getByText('Test Agent')).toBeVisible();
await expect(page.getByText(/online/i)).toBeVisible();
});
test('should display task information', async ({ page }) => {
await page.goto('/dashboard');
// Wait for tasks to load
await expect(page.getByText('Test Task')).toBeVisible();
await expect(page.getByText(/pending/i)).toBeVisible();
});
test('should navigate to different sections', async ({ page }) => {
await page.goto('/dashboard');
// Test navigation to agents page
await page.getByRole('link', { name: /agents/i }).click();
await expect(page).toHaveURL(/.*\/agents/);
// Navigate back to dashboard
await page.getByRole('link', { name: /dashboard/i }).click();
await expect(page).toHaveURL(/.*\/dashboard/);
// Test navigation to tasks page
await page.getByRole('link', { name: /tasks/i }).click();
await expect(page).toHaveURL(/.*\/tasks/);
});
test('should display system metrics', async ({ page }) => {
await page.goto('/dashboard');
// Check for metric cards
await expect(page.getByText('1')).toBeVisible(); // Total agents
await expect(page.getByText('5')).toBeVisible(); // Completed tasks
});
test('should handle real-time updates via WebSocket', async ({ page }) => {
await page.goto('/dashboard');
// Mock WebSocket connection
await page.evaluate(() => {
// Simulate WebSocket message
window.dispatchEvent(new CustomEvent('socket-message', {
detail: {
type: 'task_update',
data: {
id: 'task-1',
status: 'completed'
}
}
}));
});
// Check that the task status updates
// Note: This would require the actual WebSocket implementation
// For now, we just verify the dashboard loads properly
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
});

View File

@@ -0,0 +1,31 @@
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
console.log('🚀 Starting global setup for Playwright tests...');
// Check if backend is running
try {
const browser = await chromium.launch();
const page = await browser.newPage();
// Try to reach the backend health endpoint
try {
const response = await page.request.get('http://localhost:8087/health');
if (!response.ok()) {
console.warn('⚠️ Backend health check failed. Some tests may fail.');
} else {
console.log('✅ Backend is running and healthy');
}
} catch (error) {
console.warn('⚠️ Could not reach backend. Some tests may fail:', error);
}
await browser.close();
} catch (error) {
console.error('❌ Global setup failed:', error);
}
console.log('✅ Global setup completed');
}
export default globalSetup;