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);
});
});