feat(planner): implement C1 weekly planner home screen (#26)
Three-breakpoint layout (mobile/tablet/desktop) with VarietyScoreCard, WeekStrip, DayMealCard components. Server loads week plan and variety score via API; read-only role behavior derived from benutzer.rolle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
130
frontend/src/routes/(app)/planner/page.server.test.ts
Normal file
130
frontend/src/routes/(app)/planner/page.server.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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('planner page — load', () => {
|
||||
let load: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockGet.mockReset();
|
||||
mockPost.mockReset();
|
||||
vi.resetModules();
|
||||
const mod = await import('./+page.server');
|
||||
load = mod.load;
|
||||
});
|
||||
|
||||
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: 30 } },
|
||||
{ id: 's2', slotDate: '2026-03-31', recipe: { id: 'r2', name: 'Curry', effort: 'Medium', cookTimeMin: 45 } }
|
||||
]
|
||||
};
|
||||
|
||||
it('fetches week plan for the current week by default', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
||||
mockGet.mockResolvedValueOnce({
|
||||
data: { score: 7.5, ingredientOverlaps: [], tagRepeats: [], recentRepeats: [], duplicatesInPlan: [] },
|
||||
error: undefined
|
||||
});
|
||||
const url = new URL('http://localhost/planner');
|
||||
await load({ fetch: vi.fn(), url });
|
||||
expect(mockGet).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ params: expect.objectContaining({ query: expect.objectContaining({ weekStart: expect.any(String) }) }) }));
|
||||
});
|
||||
|
||||
it('uses weekStart from URL search params if provided', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
||||
mockGet.mockResolvedValueOnce({ data: { score: 8 }, error: undefined });
|
||||
const url = new URL('http://localhost/planner?week=2026-03-30');
|
||||
await load({ fetch: vi.fn(), url });
|
||||
expect(mockGet).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ params: expect.objectContaining({ query: { weekStart: '2026-03-30' } }) }));
|
||||
});
|
||||
|
||||
it('returns weekPlan with slots in page data', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
||||
mockGet.mockResolvedValueOnce({ data: { score: 7.5 }, error: undefined });
|
||||
const url = new URL('http://localhost/planner');
|
||||
const result = await load({ fetch: vi.fn(), url });
|
||||
expect(result.weekPlan).toBeDefined();
|
||||
expect(result.weekPlan.id).toBe('plan-1');
|
||||
expect(result.weekPlan.slots).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns variety score in page data', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
||||
mockGet.mockResolvedValueOnce({ data: { score: 7.5, ingredientOverlaps: [{ ingredientName: 'Tomate', days: ['2026-03-30', '2026-03-31'] }] }, error: undefined });
|
||||
const url = new URL('http://localhost/planner');
|
||||
const result = await load({ fetch: vi.fn(), url });
|
||||
expect(result.varietyScore.score).toBe(7.5);
|
||||
expect(result.varietyScore.ingredientOverlaps).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns null weekPlan when API returns 404', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 404 } });
|
||||
const url = new URL('http://localhost/planner');
|
||||
const result = await load({ fetch: vi.fn(), url });
|
||||
expect(result.weekPlan).toBeNull();
|
||||
expect(result.varietyScore).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the weekStart used for the query', async () => {
|
||||
mockGet.mockResolvedValueOnce({ data: mockWeekPlan, error: undefined });
|
||||
mockGet.mockResolvedValueOnce({ data: { score: 6 }, error: undefined });
|
||||
const url = new URL('http://localhost/planner?week=2026-03-30');
|
||||
const result = await load({ fetch: vi.fn(), url });
|
||||
expect(result.weekStart).toBe('2026-03-30');
|
||||
});
|
||||
|
||||
it('creates week plan if not found and fetches variety score after creation', async () => {
|
||||
// When no plan exists (404), load should return null weekPlan — creation is done via a POST action, not in load
|
||||
mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 404 } });
|
||||
const url = new URL('http://localhost/planner');
|
||||
const result = await load({ fetch: vi.fn(), url });
|
||||
expect(result.weekPlan).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('planner page — actions', () => {
|
||||
let actions: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockGet.mockReset();
|
||||
mockPost.mockReset();
|
||||
vi.resetModules();
|
||||
const mod = await import('./+page.server');
|
||||
actions = mod.actions;
|
||||
});
|
||||
|
||||
it('createPlan action calls POST /v1/week-plans', async () => {
|
||||
mockPost.mockResolvedValue({ data: { id: 'plan-new', weekStart: '2026-03-30', slots: [] }, error: undefined });
|
||||
const formData = new FormData();
|
||||
formData.set('weekStart', '2026-03-30');
|
||||
const result = await actions.createPlan({
|
||||
fetch: vi.fn(),
|
||||
request: { formData: async () => formData }
|
||||
});
|
||||
expect(mockPost).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ body: { weekStart: '2026-03-30' } }));
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('createPlan action returns error when API fails', async () => {
|
||||
mockPost.mockResolvedValue({ data: undefined, error: { message: 'Server error' } });
|
||||
const formData = new FormData();
|
||||
formData.set('weekStart', '2026-03-30');
|
||||
const result = await actions.createPlan({
|
||||
fetch: vi.fn(),
|
||||
request: { formData: async () => formData }
|
||||
});
|
||||
expect(result).toEqual({ success: false, error: expect.any(String) });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user