import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Badge } from '../components/ui/Badge.js'; import { ProgressBar } from '../components/ui/ProgressBar.js'; import { createObjectiveSchema } from '../schemas/objective.schema.js'; // ────────────────────────────────────────────────────────────────────────────── // Test 1: Component render — Badge displays correct label per status // ────────────────────────────────────────────────────────────────────────────── describe('Badge component', () => { it('renders "In Progress" label for IN_PROGRESS status', () => { render(); expect(screen.getByText('In Progress')).toBeInTheDocument(); }); it('renders "Completed" label for COMPLETED status', () => { render(); expect(screen.getByText('Completed')).toBeInTheDocument(); }); it('renders "Not Started" label for NOT_STARTED status', () => { render(); expect(screen.getByText('Not Started')).toBeInTheDocument(); }); }); // ────────────────────────────────────────────────────────────────────────────── // Test 2: ProgressBar clamps value to 0–100 range // ────────────────────────────────────────────────────────────────────────────── describe('ProgressBar component', () => { it('clamps value above 100 to 100%', () => { const { container } = render(); const bar = container.querySelector('.bg-blue-600') as HTMLElement; expect(bar.style.width).toBe('100%'); }); it('clamps negative value to 0%', () => { const { container } = render(); const bar = container.querySelector('.bg-blue-600') as HTMLElement; expect(bar.style.width).toBe('0%'); }); it('renders exact value within range', () => { const { container } = render(); const bar = container.querySelector('.bg-blue-600') as HTMLElement; expect(bar.style.width).toBe('75%'); }); }); // ────────────────────────────────────────────────────────────────────────────── // Test 3: Form validation — Zod schema enforces quarter format // ────────────────────────────────────────────────────────────────────────────── describe('createObjectiveSchema validation', () => { it('accepts a valid payload', () => { const result = createObjectiveSchema.safeParse({ title: 'Improve platform uptime', ownerId: 1, quarter: 'Q2/2026', }); expect(result.success).toBe(true); }); it('rejects invalid quarter format', () => { const result = createObjectiveSchema.safeParse({ title: 'Improve platform uptime', ownerId: 1, quarter: 'Q5/2026', }); expect(result.success).toBe(false); if (!result.success) { const quarterError = result.error.issues.find((i) => i.path[0] === 'quarter'); expect(quarterError).toBeDefined(); } }); it('rejects empty title', () => { const result = createObjectiveSchema.safeParse({ title: '', ownerId: 1, quarter: 'Q1/2025', }); expect(result.success).toBe(false); if (!result.success) { const titleError = result.error.issues.find((i) => i.path[0] === 'title'); expect(titleError).toBeDefined(); } }); it('rejects non-positive ownerId', () => { const result = createObjectiveSchema.safeParse({ title: 'Valid title', ownerId: -1, quarter: 'Q3/2025', }); expect(result.success).toBe(false); if (!result.success) { const ownerError = result.error.issues.find((i) => i.path[0] === 'ownerId'); expect(ownerError).toBeDefined(); } }); }); // ────────────────────────────────────────────────────────────────────────────── // Test 4: Progress calculation (same logic as Dashboard.objectiveProgress) // ────────────────────────────────────────────────────────────────────────────── function objectiveProgress(keyResults: { progress: number }[]): number { if (keyResults.length === 0) return 0; return Math.round(keyResults.reduce((total, kr) => total + kr.progress, 0) / keyResults.length); } describe('objectiveProgress calculation', () => { it('returns 0 for empty key results', () => { expect(objectiveProgress([])).toBe(0); }); it('computes average progress across key results', () => { expect(objectiveProgress([{ progress: 50 }, { progress: 100 }])).toBe(75); }); it('rounds fractional averages', () => { expect(objectiveProgress([{ progress: 33 }, { progress: 34 }, { progress: 34 }])).toBe(34); }); it('returns 100 when all key results complete', () => { expect(objectiveProgress([{ progress: 100 }, { progress: 100 }])).toBe(100); }); }); // ────────────────────────────────────────────────────────────────────────────── // Test 5: API error handling — axios mock returns error state // ────────────────────────────────────────────────────────────────────────────── vi.mock('axios', () => ({ default: { create: vi.fn(() => ({ get: vi.fn(), post: vi.fn(), patch: vi.fn(), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, })), }, })); describe('API error handling', () => { beforeEach(() => { vi.clearAllMocks(); }); it('reports error when API rejects', async () => { const mockGet = vi.fn().mockRejectedValue(new Error('Network Error')); const result = await mockGet('/api/objectives').catch((e: Error) => e); expect(result).toBeInstanceOf(Error); expect((result as Error).message).toBe('Network Error'); }); it('returns data when API resolves', async () => { const mockGet = vi.fn().mockResolvedValue({ data: { success: true, data: [] } }); const result = await mockGet('/api/objectives'); expect(result.data.success).toBe(true); }); });