Skip to main content
Glama
integration.test.jsx15.8 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import http from 'http'; import path from 'path'; import os from 'os'; import App from '../src/App'; // Mock os module first vi.mock('os', () => ({ default: { homedir: () => '/mock/home', tmpdir: () => '/mock/tmp' } })); // Mock fs module const mockFs = { readFile: vi.fn(), writeFile: vi.fn(), mkdir: vi.fn() }; vi.mock('fs', () => ({ promises: mockFs })); // Import server after mocks are set up const { startServer } = await import('../server.js'); describe('Integration Tests', () => { let server; let serverPort; const mockProfiles = [ { id: 'agent1', name: 'Agent 1', path: '/path/agent1.json' }, { id: 'agent2', name: 'Agent 2', path: '/path/agent2.json' } ]; const mockTasks = { tasks: [ { id: '1', name: 'Integration Test Task 1', description: 'Test task for integration testing', status: 'pending', createdAt: '2025-01-01T00:00:00Z', updatedAt: '2025-01-01T00:00:00Z', notes: 'Important notes for testing', dependencies: [{ taskId: 'dep1', name: 'Dependency 1' }], relatedFiles: [{ path: '/test.js', type: 'CREATE' }] }, { id: '2', name: 'Integration Test Task 2', description: 'Another test task', status: 'completed', createdAt: '2025-01-02T00:00:00Z', updatedAt: '2025-01-02T00:00:00Z', completedAt: '2025-01-02T12:00:00Z' } ] }; beforeEach(async () => { // Reset mocks vi.clearAllMocks(); // Setup file system mocks mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: mockProfiles })); } else if (filePath.includes('agent1.json')) { return Promise.resolve(JSON.stringify(mockTasks)); } else if (filePath.includes('agent2.json')) { return Promise.resolve(JSON.stringify({ tasks: [] })); } else if (filePath.includes('index.html')) { return Promise.resolve('<html><body>React App</body></html>'); } const error = new Error('File not found'); error.code = 'ENOENT'; return Promise.reject(error); }); mockFs.writeFile.mockResolvedValue(); mockFs.mkdir.mockResolvedValue(); // Start test server server = await startServer(); serverPort = server.address().port; // Mock fetch to use test server global.fetch = vi.fn(async (url, options) => { const response = await makeRealRequest(url, options); return { ok: response.statusCode >= 200 && response.statusCode < 300, status: response.statusCode, json: async () => JSON.parse(response.body), text: async () => response.body }; }); }); afterEach(async () => { if (server) { await new Promise((resolve) => server.close(resolve)); } vi.restoreAllMocks(); }); describe('Full Application Flow', () => { it('loads profiles and displays tasks when profile selected', async () => { render(<App />); // Wait for profiles to load await waitFor(() => { expect(screen.getByText('Agent 1')).toBeInTheDocument(); expect(screen.getByText('Agent 2')).toBeInTheDocument(); }); // Click on Agent 1 fireEvent.click(screen.getByText('Agent 1')); // Wait for tasks to load await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); expect(screen.getByText('Integration Test Task 2')).toBeInTheDocument(); }); // Verify task details are displayed expect(screen.getByText('Test task for integration testing')).toBeInTheDocument(); expect(screen.getByText('pending')).toBeInTheDocument(); expect(screen.getByText('completed')).toBeInTheDocument(); }); it('allows viewing task details and navigating back', async () => { render(<App />); // Load profile and tasks await waitFor(() => { expect(screen.getByText('Agent 1')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Agent 1')); await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); }); // Click on task to view details const taskRow = screen.getByText('Integration Test Task 1').closest('tr'); fireEvent.click(taskRow); // Verify task detail view await waitFor(() => { expect(screen.getByText('← Back to Tasks')).toBeInTheDocument(); expect(screen.getByText('Important notes for testing')).toBeInTheDocument(); expect(screen.getByText('Dependency 1')).toBeInTheDocument(); expect(screen.getByText('/test.js')).toBeInTheDocument(); }); // Navigate back fireEvent.click(screen.getByText('← Back to Tasks')); // Verify we're back to task list await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); expect(screen.queryByText('← Back to Tasks')).not.toBeInTheDocument(); }); }); it('supports searching and filtering tasks', async () => { render(<App />); // Load tasks await waitFor(() => { fireEvent.click(screen.getByText('Agent 1')); }); await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); expect(screen.getByText('Integration Test Task 2')).toBeInTheDocument(); }); // Search for specific task const searchInput = screen.getByPlaceholderText(/Search tasks/); await userEvent.type(searchInput, 'Task 1'); // Verify filtering await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); expect(screen.queryByText('Integration Test Task 2')).not.toBeInTheDocument(); }); // Clear search await userEvent.clear(searchInput); // Both tasks should be visible again await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); expect(screen.getByText('Integration Test Task 2')).toBeInTheDocument(); }); }); it('handles profile switching correctly', async () => { render(<App />); await waitFor(() => { expect(screen.getByText('Agent 1')).toBeInTheDocument(); }); // Load Agent 1 tasks fireEvent.click(screen.getByText('Agent 1')); await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); }); // Switch to Agent 2 fireEvent.click(screen.getByText('Agent 2')); // Should show no tasks message await waitFor(() => { expect(screen.getByText('No tasks to display')).toBeInTheDocument(); expect(screen.queryByText('Integration Test Task 1')).not.toBeInTheDocument(); }); // Switch back to Agent 1 fireEvent.click(screen.getByText('Agent 1')); // Tasks should be visible again await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); }); }); it('supports adding a new profile', async () => { const fs = await import('fs/promises'); render(<App />); await waitFor(() => { expect(screen.getByText('+ Add Profile')).toBeInTheDocument(); }); // Open add profile modal fireEvent.click(screen.getByText('+ Add Profile')); expect(screen.getByText('Add New Profile')).toBeInTheDocument(); // Fill in profile details const nameInput = screen.getByPlaceholderText('Profile Name'); await userEvent.type(nameInput, 'New Test Agent'); // Create and upload file const file = new File(['{"tasks":[{"id":"new1","name":"New Task"}]}'], 'tasks.json', { type: 'application/json' }); const fileInput = screen.getByLabelText(/JSON file/); await userEvent.upload(fileInput, file); // Mock the server response for add profile mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: [...mockProfiles, { id: 'new-test-agent', name: 'New Test Agent', path: '/mock/tmp/new.json' }] })); } else if (filePath.includes('new.json')) { return Promise.resolve('{"tasks":[{"id":"new1","name":"New Task"}]}'); } return Promise.reject(new Error('File not found')); }); // Submit form fireEvent.click(screen.getByText('Add Profile')); // Verify new profile appears await waitFor(() => { expect(screen.getByText('New Test Agent')).toBeInTheDocument(); }); }); it('supports removing a profile', async () => { const fs = await import('fs/promises'); render(<App />); await waitFor(() => { expect(screen.getByText('Agent 1')).toBeInTheDocument(); }); // Mock updated profiles after removal mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: [mockProfiles[1]] // Only Agent 2 remains })); } return Promise.reject(new Error('File not found')); }); // Remove Agent 1 const removeButtons = screen.getAllByLabelText(/Remove profile/); fireEvent.click(removeButtons[0]); // Verify removal await waitFor(() => { expect(screen.queryByText('Agent 1')).not.toBeInTheDocument(); expect(screen.getByText('Agent 2')).toBeInTheDocument(); }); }); }); describe('Auto-refresh Integration', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('automatically refreshes tasks at specified interval', async () => { const fs = await import('fs/promises'); let taskCallCount = 0; // Mock changing task data mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('agent1.json')) { taskCallCount++; if (taskCallCount === 1) { return Promise.resolve(JSON.stringify(mockTasks)); } else { // Return updated tasks on refresh return Promise.resolve(JSON.stringify({ tasks: [ ...mockTasks.tasks, { id: '3', name: 'New Task After Refresh', status: 'pending', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } ] })); } } else if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: mockProfiles })); } return Promise.reject(new Error('File not found')); }); render(<App />); // Select profile and enable auto-refresh await waitFor(() => { fireEvent.click(screen.getByText('Agent 1')); }); await waitFor(() => { expect(screen.getByText('Integration Test Task 1')).toBeInTheDocument(); }); const autoRefreshCheckbox = screen.getByLabelText(/Auto-refresh/); fireEvent.click(autoRefreshCheckbox); // Fast-forward time vi.advanceTimersByTime(30000); // Wait for new task to appear await waitFor(() => { expect(screen.getByText('New Task After Refresh')).toBeInTheDocument(); }); }); }); describe('Error Handling Integration', () => { it('displays error when server is unavailable', async () => { // Close server to simulate error await new Promise((resolve) => server.close(resolve)); server = null; // Mock fetch to simulate network error global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); render(<App />); await waitFor(() => { expect(screen.getByText(/Failed to load profiles/)).toBeInTheDocument(); }); }); it('handles corrupted task data gracefully', async () => { const fs = await import('fs/promises'); mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('agent1.json')) { return Promise.resolve('{ invalid json }'); } else if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: mockProfiles })); } return Promise.reject(new Error('File not found')); }); render(<App />); await waitFor(() => { fireEvent.click(screen.getByText('Agent 1')); }); await waitFor(() => { expect(screen.getByText(/Error loading tasks/)).toBeInTheDocument(); }); }); }); describe('Performance and Large Data Sets', () => { it('handles large number of tasks efficiently', async () => { const fs = await import('fs/promises'); // Generate 1000 tasks const largeTasks = { tasks: Array.from({ length: 1000 }, (_, i) => ({ id: `task-${i}`, name: `Task ${i}`, description: `Description for task ${i}`, status: i % 2 === 0 ? 'pending' : 'completed', createdAt: new Date(2025, 0, 1 + (i % 30)).toISOString(), updatedAt: new Date(2025, 0, 1 + (i % 30)).toISOString() })) }; mockFs.readFile.mockImplementation((filePath) => { if (filePath.includes('agent1.json')) { return Promise.resolve(JSON.stringify(largeTasks)); } else if (filePath.includes('.shrimp-task-viewer-settings.json')) { return Promise.resolve(JSON.stringify({ agents: mockProfiles })); } return Promise.reject(new Error('File not found')); }); render(<App />); await waitFor(() => { fireEvent.click(screen.getByText('Agent 1')); }); // Should show first page of tasks (10 by default) await waitFor(() => { expect(screen.getByText('Task 0')).toBeInTheDocument(); expect(screen.getByText('Task 9')).toBeInTheDocument(); expect(screen.queryByText('Task 10')).not.toBeInTheDocument(); }); // Verify pagination info expect(screen.getByText(/Showing 1 to 10 of 1000 tasks/)).toBeInTheDocument(); }); }); }); // Helper function to make real HTTP requests to test server function makeRealRequest(url, options = {}) { return new Promise((resolve) => { const urlObj = new URL(url); const reqOptions = { hostname: urlObj.hostname, port: urlObj.port, path: urlObj.pathname + urlObj.search, method: options.method || 'GET', headers: options.headers || {} }; const req = http.request(reqOptions, (res) => { let body = ''; res.on('data', (chunk) => body += chunk.toString()); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body }); }); }); if (options.body) { if (options.body instanceof FormData) { // Handle FormData - simplified for testing req.write('name=Test&taskFile=' + encodeURIComponent('{"tasks":[]}')); } else { req.write(options.body); } } req.end(); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cjo4m06/mcp-shrimp-task-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server