--- name: generate-test description: Generate Jest test suite with mocks and common test cases. Use when creating tests for components, repositories, or API routes. allowed-tools: Read, Write, Glob, Grep --- # Generate Test Generate a Jest test suite following Health Tracker 9000 testing patterns. ## Usage When user requests to create tests, ask for: 1. **Test target** (component, repository, API route, or utility) 2. **Target name** (e.g., "MealLogForm", "WaterLogRepository") 3. **Main functionality** to test 4. **Edge cases** or error scenarios ## Implementation Pattern Based on `src/__tests__/components/forms/MealLogForm.test.tsx` pattern. ### Component Test Structure Create file: `src/__tests__/{target-type}/{location}/{TargetName}.test.tsx` ```typescript import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { FormName } from '@/components/forms/FormName'; import { useHealthStore } from '@/lib/store/healthStore'; // Mock the store jest.mock('@/lib/store/healthStore'); jest.mock('sonner', () => ({ toast: { success: jest.fn(), error: jest.fn(), info: jest.fn(), }, })); const mockAddItem = jest.fn(); describe('FormName', () => { beforeEach(() => { jest.clearAllMocks(); (useHealthStore as jest.Mock).mockReturnValue({ addItem: mockAddItem, isLoading: false, }); }); it('renders the form correctly', () => { render(); expect(screen.getByLabelText('Field 1 Label')).toBeInTheDocument(); expect(screen.getByLabelText('Field 2 Label')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument(); }); it('validates required fields', async () => { render(); const submitButton = screen.getByRole('button', { name: /submit/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText('Field 1 is required')).toBeInTheDocument(); }); }); it('submits form with valid data', async () => { render(); const field1Input = screen.getByLabelText('Field 1 Label') as HTMLInputElement; const field2Input = screen.getByLabelText('Field 2 Label') as HTMLInputElement; const submitButton = screen.getByRole('button', { name: /submit/i }); fireEvent.change(field1Input, { target: { value: 'test value' } }); fireEvent.change(field2Input, { target: { value: '100' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockAddItem).toHaveBeenCalledWith( expect.objectContaining({ field1: 'test value', field2: '100', }) ); }); }); it('clears form after successful submission', async () => { render(); const field1Input = screen.getByLabelText('Field 1 Label') as HTMLInputElement; const submitButton = screen.getByRole('button', { name: /submit/i }); fireEvent.change(field1Input, { target: { value: 'test' } }); fireEvent.click(submitButton); await waitFor(() => { expect(field1Input.value).toBe(''); }); }); it('disables form while loading', () => { (useHealthStore as jest.Mock).mockReturnValue({ addItem: mockAddItem, isLoading: true, }); render(); const submitButton = screen.getByRole('button', { name: /submit/i }); expect(submitButton).toBeDisabled(); }); }); ``` ### Repository Test Structure Create file: `src/__tests__/lib/database/repositories/{EntityName}.test.ts` ```typescript import { EntityRepository } from '@/lib/database/repositories/entityRepository'; import { getDatabase } from '@/lib/database/connection'; jest.mock('@/lib/database/connection'); describe('EntityRepository', () => { let repo: EntityRepository; let mockDb: any; beforeEach(() => { jest.clearAllMocks(); mockDb = { prepare: jest.fn(), }; (getDatabase as jest.Mock).mockReturnValue(mockDb); repo = new EntityRepository(); }); it('adds entity correctly', () => { const mockStmt = { run: jest.fn() }; mockDb.prepare.mockReturnValue(mockStmt); const result = repo.addEntity({ field1: 'value1', field2: 'value2' }); expect(result).toHaveProperty('id'); expect(result).toHaveProperty('createdAt'); expect(result.field1).toBe('value1'); expect(mockStmt.run).toHaveBeenCalled(); }); it('gets entity by id', () => { const mockStmt = { get: jest.fn() }; mockDb.prepare.mockReturnValue(mockStmt); mockStmt.get.mockReturnValue({ id: '123', field_1: 'value1', field_2: '{"nested": "data"}', }); const result = repo.getEntityById('123'); expect(result).toEqual( expect.objectContaining({ id: '123', field1: 'value1', }) ); }); it('returns null for non-existent entity', () => { const mockStmt = { get: jest.fn() }; mockDb.prepare.mockReturnValue(mockStmt); mockStmt.get.mockReturnValue(undefined); const result = repo.getEntityById('nonexistent'); expect(result).toBeNull(); }); it('throws error when updating non-existent entity', () => { const mockStmt = { get: jest.fn() }; mockDb.prepare.mockReturnValue(mockStmt); mockStmt.get.mockReturnValue(undefined); expect(() => { repo.updateEntity('nonexistent', {}); }).toThrow('Entity not found'); }); it('deletes entity correctly', () => { const mockStmt = { run: jest.fn() }; mockDb.prepare.mockReturnValue(mockStmt); repo.deleteEntity('123'); expect(mockStmt.run).toHaveBeenCalledWith('123'); }); }); ``` ### API Route Test Structure Create file: `src/__tests__/app/api/{resource}/route.test.ts` ```typescript import { POST, DELETE } from '@/app/api/resource/route'; import { ResourceRepository } from '@/lib/database/repositories/resourceRepository'; jest.mock('@/lib/database/repositories/resourceRepository'); jest.mock('@/lib/database/repositories/dailySummaryRepository'); describe('Resource API Route', () => { beforeEach(() => { jest.clearAllMocks(); }); it('POST creates new resource', async () => { const mockRepo = { addResource: jest.fn().mockReturnValue({ id: '123', field: 'value' }), }; (ResourceRepository as jest.Mock).mockImplementation(() => mockRepo); const request = new Request('http://localhost:3000/api/resource', { method: 'POST', body: JSON.stringify({ field: 'value', date: '2024-01-15' }), }); const response = await POST(request); const data = await response.json(); expect(response.status).toBe(200); expect(data).toEqual({ id: '123', field: 'value' }); }); it('DELETE removes resource', async () => { const mockRepo = { deleteResource: jest.fn() }; (ResourceRepository as jest.Mock).mockImplementation(() => mockRepo); const request = new Request('http://localhost:3000/api/resource?id=123', { method: 'DELETE', }); const response = await DELETE(request); expect(response.status).toBe(200); expect(mockRepo.deleteResource).toHaveBeenCalledWith('123'); }); it('DELETE returns 400 when id missing', async () => { const request = new Request('http://localhost:3000/api/resource', { method: 'DELETE', }); const response = await DELETE(request); expect(response.status).toBe(400); }); }); ``` ## Key Conventions - Test file location mirrors source structure in `src/__tests__/` - File naming: `{SourceName}.test.ts(x)` - Mock external dependencies (stores, API calls, database) - beforeEach to clear mocks between tests - Use React Testing Library patterns for components - Test user interactions, not implementation - Test error cases and edge cases - Use descriptive test names (should be readable as documentation) - Mock data should be realistic ## Test Coverage Targets For components: - Renders correctly - Handles user input - Validates data - Shows error states - Manages loading states - Calls store actions For repositories: - CRUD operations work - Row mapping correct - Error handling - Null checks For API routes: - POST creates records - GET retrieves records - DELETE removes records - Error handling (400, 500) - Proper HTTP status codes ## Steps 1. Ask user for test target, name, and functionality 2. Create file: `src/__tests__/{path}/{Name}.test.ts(x)` 3. Mock dependencies (stores, database, APIs) 4. Write beforeEach to clear mocks 5. Write test cases for main functionality 6. Write test cases for error scenarios 7. Format with Prettier ## Implementation Checklist - [ ] Test file in correct location - [ ] Dependencies properly mocked - [ ] beforeEach clears mocks - [ ] Tests use descriptive names - [ ] Main functionality tested - [ ] Error cases tested - [ ] Edge cases tested - [ ] Uses appropriate testing library - [ ] No implementation details tested - [ ] Proper assertions used