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

185
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,185 @@
import axios from 'axios';
// Types
export interface LoginRequest {
email: string;
password: string;
}
export interface RegisterRequest {
email: string;
password: string;
full_name: string;
username: string;
}
export interface AuthResponse {
access_token: string;
token_type: string;
user: User;
}
export interface User {
id: string;
username: string;
email: string;
full_name?: string;
name?: string; // For backward compatibility
role?: string;
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
}
export interface APIKey {
id: string;
name: string;
key_prefix: string;
scopes: string[];
created_at: string;
last_used?: string;
expires_at?: string;
is_active: boolean;
}
export interface CreateAPIKeyRequest {
name: string;
scopes: string[];
expires_in_days?: number;
}
export interface CreateAPIKeyResponse {
api_key: APIKey;
key: string; // Full key (only returned once)
}
// API client
const apiClient = axios.create({
baseURL: process.env.VITE_API_BASE_URL || 'http://localhost:8087',
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Clear token and redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Auth API functions
export const login = async (credentials: LoginRequest): Promise<AuthResponse> => {
const response = await apiClient.post('/api/auth/login', credentials);
return response.data;
};
export const register = async (userData: RegisterRequest): Promise<AuthResponse> => {
const response = await apiClient.post('/api/auth/register', userData);
return response.data;
};
export const getCurrentUser = async (): Promise<User> => {
const response = await apiClient.get('/api/auth/me');
return response.data;
};
export const logout = async (): Promise<void> => {
try {
await apiClient.post('/api/auth/logout');
} finally {
// Always clear local storage even if API call fails
localStorage.removeItem('token');
}
};
export const refreshToken = async (): Promise<AuthResponse> => {
const response = await apiClient.post('/api/auth/refresh');
return response.data;
};
// API Key management
export const getAPIKeys = async (): Promise<APIKey[]> => {
const response = await apiClient.get('/api/auth/api-keys');
return response.data;
};
export const createAPIKey = async (data: CreateAPIKeyRequest): Promise<CreateAPIKeyResponse> => {
const response = await apiClient.post('/api/auth/api-keys', data);
return response.data;
};
export const revokeAPIKey = async (keyId: string): Promise<void> => {
await apiClient.delete(`/api/auth/api-keys/${keyId}`);
};
export const updateAPIKey = async (keyId: string, data: Partial<CreateAPIKeyRequest>): Promise<APIKey> => {
const response = await apiClient.put(`/api/auth/api-keys/${keyId}`, data);
return response.data;
};
// Password management
export const changePassword = async (oldPassword: string, newPassword: string): Promise<void> => {
await apiClient.post('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword,
});
};
export const requestPasswordReset = async (email: string): Promise<void> => {
await apiClient.post('/api/auth/forgot-password', { email });
};
export const resetPassword = async (token: string, newPassword: string): Promise<void> => {
await apiClient.post('/api/auth/reset-password', {
token,
new_password: newPassword,
});
};
// Profile management
export const updateProfile = async (data: Partial<User>): Promise<User> => {
const response = await apiClient.put('/api/auth/profile', data);
return response.data;
};
export const verifyEmail = async (token: string): Promise<void> => {
await apiClient.post('/api/auth/verify-email', { token });
};
export const resendVerificationEmail = async (): Promise<void> => {
await apiClient.post('/api/auth/resend-verification');
};
export default {
login,
register,
getCurrentUser,
logout,
refreshToken,
getAPIKeys,
createAPIKey,
revokeAPIKey,
updateAPIKey,
changePassword,
requestPasswordReset,
resetPassword,
updateProfile,
verifyEmail,
resendVerificationEmail,
};

View File

@@ -0,0 +1,140 @@
import { screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, mockApiResponses } from '../../../test/utils';
import LoginForm from '../LoginForm';
import * as authApi from '../../../api/auth';
// Mock the auth API
jest.mock('../../../api/auth');
const mockAuthApi = authApi as jest.Mocked<typeof authApi>;
// Mock react-router-dom
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('LoginForm', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders login form with email and password fields', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
it('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<LoginForm />);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
});
it('shows validation error for invalid email format', async () => {
const user = userEvent.setup();
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'invalid-email');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid email format/i)).toBeInTheDocument();
});
});
it('submits form with valid credentials', async () => {
const user = userEvent.setup();
mockAuthApi.login.mockResolvedValue(mockApiResponses.auth.login);
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
await waitFor(() => {
expect(mockAuthApi.login).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
it('shows error message on login failure', async () => {
const user = userEvent.setup();
mockAuthApi.login.mockRejectedValue(new Error('Invalid credentials'));
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'wrongpassword');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
});
});
it('disables submit button while loading', async () => {
const user = userEvent.setup();
// Mock a slow API response
mockAuthApi.login.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve(mockApiResponses.auth.login), 1000))
);
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
// Check that button is disabled during loading
expect(submitButton).toBeDisabled();
expect(screen.getByText(/signing in/i)).toBeInTheDocument();
});
it('toggles password visibility', async () => {
const user = userEvent.setup();
render(<LoginForm />);
const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement;
const toggleButton = screen.getByRole('button', { name: /toggle password visibility/i });
// Initially password should be hidden
expect(passwordInput.type).toBe('password');
// Click toggle to show password
await user.click(toggleButton);
expect(passwordInput.type).toBe('text');
// Click toggle again to hide password
await user.click(toggleButton);
expect(passwordInput.type).toBe('password');
});
});

View File

@@ -0,0 +1,178 @@
import { renderHook, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider, useAuth } from '../AuthContext';
import * as authApi from '../../api/auth';
import { mockApiResponses, mockUser } from '../../test/utils';
// Mock the auth API
jest.mock('../../api/auth');
const mockAuthApi = authApi as jest.Mocked<typeof authApi>;
// Mock localStorage
const mockLocalStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
});
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
);
};
describe('AuthContext', () => {
beforeEach(() => {
jest.clearAllMocks();
mockLocalStorage.getItem.mockReturnValue(null);
});
it('initializes with no user when no token in localStorage', () => {
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.isLoading).toBe(false);
});
it('attempts to load user when token exists in localStorage', async () => {
mockLocalStorage.getItem.mockReturnValue('mock-token');
mockAuthApi.getCurrentUser.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
// Initially loading
expect(result.current.isLoading).toBe(true);
// Wait for the async operation to complete
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.isLoading).toBe(false);
});
it('clears user data when token is invalid', async () => {
mockLocalStorage.getItem.mockReturnValue('invalid-token');
mockAuthApi.getCurrentUser.mockRejectedValue(new Error('Unauthorized'));
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('token');
});
it('logs in user successfully', async () => {
mockAuthApi.login.mockResolvedValue(mockApiResponses.auth.login);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await act(async () => {
await result.current.login('test@example.com', 'password123');
});
expect(mockAuthApi.login).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
expect(mockLocalStorage.setItem).toHaveBeenCalledWith('token', 'mock-jwt-token');
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
});
it('throws error on login failure', async () => {
const loginError = new Error('Invalid credentials');
mockAuthApi.login.mockRejectedValue(loginError);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await expect(act(async () => {
await result.current.login('test@example.com', 'wrongpassword');
})).rejects.toThrow('Invalid credentials');
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
it('logs out user successfully', async () => {
// First set up an authenticated user
mockLocalStorage.getItem.mockReturnValue('mock-token');
mockAuthApi.getCurrentUser.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
// Now logout
await act(async () => {
result.current.logout();
});
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('token');
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
it('updates user data', async () => {
// First set up an authenticated user
mockLocalStorage.getItem.mockReturnValue('mock-token');
mockAuthApi.getCurrentUser.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
const updatedUser = { ...mockUser, full_name: 'Updated Name' };
act(() => {
result.current.updateUser(updatedUser);
});
expect(result.current.user).toEqual(updatedUser);
});
it('handles register functionality', async () => {
const registerData = {
email: 'newuser@example.com',
password: 'password123',
full_name: 'New User',
username: 'newuser',
};
mockAuthApi.register.mockResolvedValue(mockApiResponses.auth.login);
const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
await act(async () => {
await result.current.register(registerData);
});
expect(mockAuthApi.register).toHaveBeenCalledWith(registerData);
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
});
});

View File

@@ -0,0 +1,265 @@
import { renderHook, act } from '@testing-library/react';
import { useSocketIO } from '../useSocketIO';
import { io } from 'socket.io-client';
// Mock socket.io-client
jest.mock('socket.io-client');
const mockIo = io as jest.MockedFunction<typeof io>;
// Mock socket instance
const mockSocket = {
connected: false,
on: jest.fn(),
emit: jest.fn(),
disconnect: jest.fn(),
onAny: jest.fn(),
};
describe('useSocketIO', () => {
beforeEach(() => {
jest.clearAllMocks();
mockSocket.connected = false;
mockIo.mockReturnValue(mockSocket as any);
});
it('initializes with disconnected state', () => {
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: false,
}));
expect(result.current.isConnected).toBe(false);
expect(result.current.connectionState).toBe('disconnected');
expect(result.current.socket).toBe(null);
expect(result.current.lastMessage).toBe(null);
});
it('auto-connects when autoConnect is true', () => {
renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
expect(mockIo).toHaveBeenCalledWith('http://localhost:8087', expect.objectContaining({
transports: ['websocket', 'polling'],
upgrade: true,
rememberUpgrade: true,
autoConnect: true,
reconnection: true,
timeout: 20000,
forceNew: false,
}));
});
it('does not auto-connect when autoConnect is false', () => {
renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: false,
}));
expect(mockIo).not.toHaveBeenCalled();
});
it('handles connection events', () => {
const onConnect = jest.fn();
const onDisconnect = jest.fn();
const onError = jest.fn();
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
onConnect,
onDisconnect,
onError,
}));
// Simulate connection
const connectHandler = mockSocket.on.mock.calls.find(call => call[0] === 'connect')[1];
act(() => {
connectHandler();
});
expect(result.current.isConnected).toBe(true);
expect(result.current.connectionState).toBe('connected');
expect(onConnect).toHaveBeenCalled();
// Simulate disconnection
const disconnectHandler = mockSocket.on.mock.calls.find(call => call[0] === 'disconnect')[1];
act(() => {
disconnectHandler('transport close');
});
expect(result.current.isConnected).toBe(false);
expect(result.current.connectionState).toBe('disconnected');
expect(onDisconnect).toHaveBeenCalled();
// Simulate error
const errorHandler = mockSocket.on.mock.calls.find(call => call[0] === 'connect_error')[1];
act(() => {
errorHandler(new Error('Connection failed'));
});
expect(result.current.connectionState).toBe('error');
expect(onError).toHaveBeenCalledWith(new Error('Connection failed'));
});
it('handles message events', () => {
const onMessage = jest.fn();
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
onMessage,
}));
// Simulate generic message
const anyHandler = mockSocket.onAny.mock.calls[0][0];
act(() => {
anyHandler('task_update', { id: 'task1', status: 'completed' });
});
expect(result.current.lastMessage).toMatchObject({
type: 'task_update',
data: { id: 'task1', status: 'completed' },
});
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: 'task_update',
data: { id: 'task1', status: 'completed' },
})
);
});
it('sends messages when connected', () => {
mockSocket.connected = true;
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
act(() => {
result.current.sendMessage('test_event', { data: 'test' });
});
expect(mockSocket.emit).toHaveBeenCalledWith('test_event', { data: 'test' });
});
it('does not send messages when not connected', () => {
mockSocket.connected = false;
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
act(() => {
result.current.sendMessage('test_event', { data: 'test' });
});
expect(mockSocket.emit).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
'Socket.IO is not connected. Cannot send message:',
{ event: 'test_event', data: { data: 'test' } }
);
consoleSpy.mockRestore();
});
it('joins and leaves rooms', () => {
mockSocket.connected = true;
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
act(() => {
result.current.joinRoom('task_updates');
});
expect(mockSocket.emit).toHaveBeenCalledWith('join_room', { room: 'task_updates' });
act(() => {
result.current.leaveRoom('task_updates');
});
expect(mockSocket.emit).toHaveBeenCalledWith('leave_room', { room: 'task_updates' });
});
it('subscribes to events', () => {
mockSocket.connected = true;
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
act(() => {
result.current.subscribe(['task_update', 'agent_status'], 'monitoring');
});
expect(mockSocket.emit).toHaveBeenCalledWith('subscribe', {
events: ['task_update', 'agent_status'],
room: 'monitoring',
});
});
it('handles manual connect and disconnect', () => {
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: false,
}));
// Manual connect
act(() => {
result.current.connect();
});
expect(mockIo).toHaveBeenCalled();
expect(result.current.connectionState).toBe('connecting');
// Manual disconnect
act(() => {
result.current.disconnect();
});
expect(mockSocket.disconnect).toHaveBeenCalled();
expect(result.current.isConnected).toBe(false);
expect(result.current.connectionState).toBe('disconnected');
});
it('handles reconnection', () => {
const { result } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
// Simulate initial connection
const connectHandler = mockSocket.on.mock.calls.find(call => call[0] === 'connect')[1];
act(() => {
connectHandler();
});
// Trigger reconnect
act(() => {
result.current.reconnect();
});
expect(mockSocket.disconnect).toHaveBeenCalled();
// Should attempt to reconnect after a short delay
});
it('cleans up on unmount', () => {
const { unmount } = renderHook(() => useSocketIO({
url: 'http://localhost:8087',
autoConnect: true,
}));
unmount();
expect(mockSocket.disconnect).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,65 @@
import '@testing-library/jest-dom';
// Mock environment variables
process.env.VITE_API_BASE_URL = 'http://localhost:8087';
// Mock IntersectionObserver
global.IntersectionObserver = jest.fn().mockImplementation((callback) => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
trigger: (entries: any) => callback(entries),
}));
// Mock ResizeObserver
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock scrollTo
global.scrollTo = jest.fn();
// Mock console methods to reduce noise in tests
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
console.error = (...args: any[]) => {
// Suppress specific React warnings in tests
const message = args[0];
if (
typeof message === 'string' &&
(message.includes('Warning: ReactDOM.render is deprecated') ||
message.includes('Warning: validateDOMNesting'))
) {
return;
}
originalConsoleError.apply(console, args);
};
console.warn = (...args: any[]) => {
const message = args[0];
if (
typeof message === 'string' &&
message.includes('componentWillReceiveProps has been renamed')
) {
return;
}
originalConsoleWarn.apply(console, args);
};

120
frontend/src/test/utils.tsx Normal file
View File

@@ -0,0 +1,120 @@
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '../contexts/AuthContext';
// Create a custom render function that includes providers
const AllProviders = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
{children}
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
);
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };
// Mock data helpers
export const mockUser = {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
full_name: 'Test User',
name: 'Test User',
role: 'user',
is_active: true,
is_superuser: false,
is_verified: true,
};
export const mockAgent = {
id: 'test-agent-1',
name: 'Test Agent',
endpoint: 'http://localhost:11434',
model: 'llama3.1:8b',
specialty: 'general_ai',
max_concurrent: 2,
current_tasks: 0,
agent_type: 'ollama',
status: 'online',
last_heartbeat: Date.now(),
};
export const mockTask = {
id: 'task_1735589200_0',
title: 'Test Task',
description: 'A test task for unit testing',
type: 'general_ai',
priority: 3,
status: 'pending',
created_at: Date.now(),
context: {
objective: 'Test objective',
requirements: ['Test requirement 1'],
},
};
export const mockWorkflow = {
id: 'workflow_1735589200',
name: 'Test Workflow',
description: 'A test workflow',
steps: [
{
name: 'Step 1',
type: 'general_ai',
agent_type: 'general_ai',
inputs: { prompt: 'Test prompt' },
outputs: ['result'],
},
],
created_at: Date.now(),
status: 'pending',
};
// Mock API responses
export const mockApiResponses = {
auth: {
login: {
access_token: 'mock-jwt-token',
token_type: 'bearer',
user: mockUser,
},
me: mockUser,
},
agents: {
list: [mockAgent],
detail: mockAgent,
},
tasks: {
list: [mockTask],
detail: mockTask,
},
workflows: {
list: [mockWorkflow],
detail: mockWorkflow,
},
};
// Async utility for waiting for elements
export const waitFor = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));