feat(suggestions): implement C2 meal suggestion screen (Issue #27) Co-authored-by: Marcel Raddatz <marcel@raddatz.cloud> Co-committed-by: Marcel Raddatz <marcel@raddatz.cloud>
217 lines
8.7 KiB
TypeScript
217 lines
8.7 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();
|
|
const mockPost = vi.fn();
|
|
vi.mock('$lib/server/api', () => ({
|
|
apiClient: () => ({ GET: mockGet, POST: mockPost })
|
|
}));
|
|
|
|
describe('suggestions page — load', () => {
|
|
let load: any;
|
|
|
|
const mockSuggestions = {
|
|
suggestions: [
|
|
{
|
|
recipe: { id: 'r1', name: 'Pasta al Limone', effort: 'Easy', cookTimeMin: 25 },
|
|
simulatedScore: 9.2
|
|
},
|
|
{
|
|
recipe: { id: 'r2', name: 'Hühnchen Curry', effort: 'Medium', cookTimeMin: 45 },
|
|
simulatedScore: 6.1
|
|
}
|
|
]
|
|
};
|
|
|
|
const mockWeekPlan = {
|
|
id: 'plan-1',
|
|
weekStart: '2026-03-30',
|
|
status: 'draft',
|
|
slots: [
|
|
{ id: 's1', slotDate: '2026-03-30', recipe: { id: 'r3', name: 'Pasta', effort: 'Easy' } }
|
|
]
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
mockGet.mockReset();
|
|
mockPost.mockReset();
|
|
vi.resetModules();
|
|
const mod = await import('./+page.server');
|
|
load = mod.load;
|
|
});
|
|
|
|
it('fetches suggestions for the given plan and day', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: mockSuggestions, error: undefined });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(mockGet).toHaveBeenCalledWith('/v1/week-plans/{id}/suggestions', expect.objectContaining({
|
|
params: expect.objectContaining({ path: { id: 'plan-1' } })
|
|
}));
|
|
});
|
|
|
|
it('returns suggestions list sorted by simulatedScore descending', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: mockSuggestions, error: undefined });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.suggestions[0].recipe.name).toBe('Pasta al Limone');
|
|
expect(result.suggestions[1].recipe.name).toBe('Hühnchen Curry');
|
|
});
|
|
|
|
it('returns the selectedDay from URL params', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: mockSuggestions, error: undefined });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.selectedDay).toBe('2026-04-01');
|
|
});
|
|
|
|
it('returns empty suggestions when API fails', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 500 } });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.suggestions).toEqual([]);
|
|
});
|
|
|
|
it('returns week plan slots for context display', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: mockSuggestions, error: undefined });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.weekPlan).toBeDefined();
|
|
expect(result.weekPlan.slots).toHaveLength(1);
|
|
});
|
|
|
|
it('returns null weekPlan and empty suggestions when week plan not found', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 404 } });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30&day=2026-04-01');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.weekPlan).toBeNull();
|
|
expect(result.suggestions).toEqual([]);
|
|
});
|
|
|
|
it('defaults day to weekStart when no day param provided', async () => {
|
|
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
|
mockGet.mockResolvedValueOnce({ data: mockSuggestions, error: undefined });
|
|
const url = new URL('http://localhost/planner/suggestions?week=2026-03-30');
|
|
const result = await load({ fetch: vi.fn(), url, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } } });
|
|
expect(result.selectedDay).toBe('2026-03-30');
|
|
});
|
|
});
|
|
|
|
describe('suggestions page — pickSuggestion action', () => {
|
|
let actions: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGet.mockReset();
|
|
mockPost.mockReset();
|
|
vi.resetModules();
|
|
const mod = await import('./+page.server');
|
|
actions = mod.actions;
|
|
});
|
|
|
|
it('adds a slot to the week plan via POST and redirects to planner', async () => {
|
|
mockPost.mockResolvedValue({ data: { id: 's-new', slotDate: '2026-04-01', recipe: {} }, error: undefined });
|
|
const formData = new FormData();
|
|
formData.set('planId', 'plan-1');
|
|
formData.set('recipeId', '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f');
|
|
formData.set('slotDate', '2026-04-01');
|
|
formData.set('weekStart', '2026-03-30');
|
|
try {
|
|
await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } }
|
|
});
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.status).toBe(303);
|
|
expect(e.location).toBe('/planner?week=2026-03-30');
|
|
}
|
|
expect(mockPost).toHaveBeenCalledWith('/v1/week-plans/{id}/slots', expect.objectContaining({
|
|
params: { path: { id: 'plan-1' } },
|
|
body: { slotDate: '2026-04-01', recipeId: '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f' }
|
|
}));
|
|
});
|
|
|
|
it('returns error when planId is missing', async () => {
|
|
const formData = new FormData();
|
|
formData.set('planId', '');
|
|
formData.set('recipeId', '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f');
|
|
formData.set('slotDate', '2026-04-01');
|
|
formData.set('weekStart', '2026-03-30');
|
|
const result = await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } }
|
|
});
|
|
expect(result).toEqual({ success: false, error: 'Fehlende Plan-ID.' });
|
|
expect(mockPost).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns error for invalid recipeId format', async () => {
|
|
const formData = new FormData();
|
|
formData.set('planId', 'plan-1');
|
|
formData.set('recipeId', 'not-a-uuid');
|
|
formData.set('slotDate', '2026-04-01');
|
|
formData.set('weekStart', '2026-03-30');
|
|
const result = await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } }
|
|
});
|
|
expect(result).toEqual({ success: false, error: 'Ungültige Rezept-ID.' });
|
|
expect(mockPost).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns error when API fails', async () => {
|
|
mockPost.mockResolvedValue({ data: undefined, error: { message: 'Server error' } });
|
|
const formData = new FormData();
|
|
formData.set('planId', 'plan-1');
|
|
formData.set('recipeId', '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f');
|
|
formData.set('slotDate', '2026-04-01');
|
|
formData.set('weekStart', '2026-03-30');
|
|
const result = await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } }
|
|
});
|
|
expect(result).toEqual({ success: false, error: expect.any(String) });
|
|
});
|
|
|
|
it('returns permission error for member role', async () => {
|
|
const formData = new FormData();
|
|
formData.set('planId', 'plan-1');
|
|
formData.set('recipeId', '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f');
|
|
formData.set('slotDate', '2026-04-01');
|
|
formData.set('weekStart', '2026-03-30');
|
|
const result = await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'mitglied' } }
|
|
});
|
|
expect(result).toEqual({ success: false, error: 'Keine Berechtigung.' });
|
|
expect(mockPost).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns error for invalid slotDate format', async () => {
|
|
const formData = new FormData();
|
|
formData.set('planId', 'plan-1');
|
|
formData.set('recipeId', '1a2b3c4d-1234-1234-1234-1a2b3c4d5e6f');
|
|
formData.set('slotDate', 'not-a-date');
|
|
formData.set('weekStart', '2026-03-30');
|
|
const result = await actions.pickSuggestion({
|
|
fetch: vi.fn(),
|
|
request: { formData: async () => formData },
|
|
locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' } }
|
|
});
|
|
expect(result).toEqual({ success: false, error: expect.any(String) });
|
|
expect(mockPost).not.toHaveBeenCalled();
|
|
});
|
|
});
|