Files
mealprep/frontend/src/routes/(app)/planner/variety/page.server.test.ts
Marcel Raddatz 8ad636f825 feat(variety): implement C3 variety review screen (Issue #28)
- Add /planner/variety route with mobile stacked + desktop 2-column layout
- Implement VarietyScoreHero: Fraunces score display + progress bar + color-coded description
- Implement ScoreBreakdownList: 3 sub-score rows (protein diversity, ingredient overlap, effort balance)
- Implement VarietyWarningCards: yellow-tint warning cards derived from API tagRepeats/ingredientOverlaps
- Implement EffortBar: proportional colored segments (Easy/Medium/Hard) with ×N labels
- Desktop: protein grid (7 columns, repeat highlight with yellow ring) + effort bar in right panel
- Client-side sub-score derivation from VarietyScoreResponse (tagged for TODO to move to API)
- 26 new tests across 5 components + server load function; 455 tests total, 0 type errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 11:23:29 +02:00

99 lines
3.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$env/dynamic/private', () => ({
env: { BACKEND_URL: 'http://localhost:8080' }
}));
const mockGet = vi.fn();
vi.mock('$lib/server/api', () => ({
apiClient: () => ({ GET: mockGet })
}));
const mockVarietyScore = {
score: 8.2,
tagRepeats: [
{ tagName: 'Chicken', tagType: 'protein', days: ['MON', 'WED'] }
],
ingredientOverlaps: [
{ ingredientName: 'Tomaten', days: ['MON', 'TUE', 'WED'] }
],
recentRepeats: ['Pasta Bolognese'],
duplicatesInPlan: ['Hühnchen Curry']
};
const mockWeekPlan = {
id: 'plan-1',
weekStart: '2026-03-30',
status: 'draft',
slots: [
{ id: 's1', slotDate: '2026-03-30', recipe: { id: 'r1', name: 'Pasta', effort: 'Easy', cookTimeMin: 20 } },
{ id: 's2', slotDate: '2026-03-31', recipe: { id: 'r2', name: 'Curry', effort: 'Medium', cookTimeMin: 45 } },
{ id: 's3', slotDate: '2026-04-01', recipe: { id: 'r3', name: 'Steak', effort: 'Hard', cookTimeMin: 60 } }
]
};
describe('variety page — load', () => {
let load: any;
beforeEach(async () => {
mockGet.mockReset();
vi.resetModules();
const mod = await import('./+page.server');
load = mod.load;
});
it('fetches week plan and variety score', async () => {
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
mockGet.mockResolvedValueOnce({ data: mockVarietyScore, error: undefined });
const url = new URL('http://localhost/planner/variety?week=2026-03-30');
await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Planer', rolle: 'planer' } } });
expect(mockGet).toHaveBeenCalledWith('/v1/week-plans', expect.anything());
expect(mockGet).toHaveBeenCalledWith('/v1/week-plans/{id}/variety-score', expect.objectContaining({
params: { path: { id: 'plan-1' } }
}));
});
it('returns varietyScore and weekPlan in result', async () => {
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
mockGet.mockResolvedValueOnce({ data: mockVarietyScore, error: undefined });
const url = new URL('http://localhost/planner/variety?week=2026-03-30');
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Planer', rolle: 'planer' } } });
expect(result.varietyScore?.score).toBe(8.2);
expect(result.weekPlan?.id).toBe('plan-1');
});
it('returns weekStart from URL param', async () => {
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
mockGet.mockResolvedValueOnce({ data: mockVarietyScore, error: undefined });
const url = new URL('http://localhost/planner/variety?week=2026-03-30');
const result = await load({ fetch: vi.fn(), url, locals: {} });
expect(result.weekStart).toBe('2026-03-30');
});
it('returns null data when week plan not found', async () => {
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 404 } });
const url = new URL('http://localhost/planner/variety?week=2026-03-30');
const result = await load({ fetch: vi.fn(), url, locals: {} });
expect(result.weekPlan).toBeNull();
expect(result.varietyScore).toBeNull();
});
it('returns null varietyScore when score endpoint fails', async () => {
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 500 } });
const url = new URL('http://localhost/planner/variety?week=2026-03-30');
const result = await load({ fetch: vi.fn(), url, locals: {} });
expect(result.weekPlan?.id).toBe('plan-1');
expect(result.varietyScore).toBeNull();
});
it('uses current week when no week param provided', async () => {
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
mockGet.mockResolvedValueOnce({ data: mockVarietyScore, error: undefined });
const url = new URL('http://localhost/planner/variety');
const result = await load({ fetch: vi.fn(), url, locals: {} });
// weekStart should be a valid YYYY-MM-DD
expect(result.weekStart).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});