feat(staples): load categories and ingredients, group by category
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
28
frontend/src/routes/household/staples/+page.server.ts
Normal file
28
frontend/src/routes/household/staples/+page.server.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { apiClient } from '$lib/server/api';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
|
const api = apiClient(fetch);
|
||||||
|
|
||||||
|
const [categoriesResult, ingredientsResult] = await Promise.all([
|
||||||
|
api.GET('/v1/ingredient-categories'),
|
||||||
|
api.GET('/v1/ingredients')
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rawCategories = categoriesResult.data ?? [];
|
||||||
|
const rawIngredients = ingredientsResult.data ?? [];
|
||||||
|
|
||||||
|
const categories = rawCategories.map((cat) => ({
|
||||||
|
id: cat.id!,
|
||||||
|
name: cat.name!,
|
||||||
|
ingredients: rawIngredients
|
||||||
|
.filter((ing) => ing.category?.id === cat.id)
|
||||||
|
.map((ing) => ({
|
||||||
|
id: ing.id!,
|
||||||
|
name: ing.name!,
|
||||||
|
isStaple: ing.isStaple ?? false
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { categories };
|
||||||
|
};
|
||||||
87
frontend/src/routes/household/staples/page.server.test.ts
Normal file
87
frontend/src/routes/household/staples/page.server.test.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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 mockCategories = [
|
||||||
|
{ id: 'cat-1', name: 'Öle & Fette' },
|
||||||
|
{ id: 'cat-2', name: 'Gewürze' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockIngredients = [
|
||||||
|
{ id: 'ing-1', name: 'Olivenöl', isStaple: true, category: { id: 'cat-1', name: 'Öle & Fette' } },
|
||||||
|
{ id: 'ing-2', name: 'Butter', isStaple: false, category: { id: 'cat-1', name: 'Öle & Fette' } },
|
||||||
|
{ id: 'ing-3', name: 'Salz', isStaple: true, category: { id: 'cat-2', name: 'Gewürze' } }
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('household staples page — load', () => {
|
||||||
|
let load: any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockGet.mockReset();
|
||||||
|
vi.resetModules();
|
||||||
|
const mod = await import('./+page.server');
|
||||||
|
load = mod.load;
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockApiResponses() {
|
||||||
|
mockGet.mockImplementation((path: string) => {
|
||||||
|
if (path === '/v1/ingredient-categories') {
|
||||||
|
return Promise.resolve({ data: mockCategories, error: undefined });
|
||||||
|
}
|
||||||
|
if (path === '/v1/ingredients') {
|
||||||
|
return Promise.resolve({ data: mockIngredients, error: undefined });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('fetches both categories and ingredients in parallel', async () => {
|
||||||
|
mockApiResponses();
|
||||||
|
await load({ fetch: vi.fn() } as any);
|
||||||
|
|
||||||
|
const calls = mockGet.mock.calls.map((c) => c[0]);
|
||||||
|
expect(calls).toContain('/v1/ingredient-categories');
|
||||||
|
expect(calls).toContain('/v1/ingredients');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('groups ingredients by category id', async () => {
|
||||||
|
mockApiResponses();
|
||||||
|
const result = await load({ fetch: vi.fn() } as any);
|
||||||
|
|
||||||
|
expect(result.categories).toHaveLength(2);
|
||||||
|
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
||||||
|
expect(oele.ingredients).toHaveLength(2);
|
||||||
|
expect(oele.ingredients[0].name).toBe('Olivenöl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves isStaple flag on each ingredient', async () => {
|
||||||
|
mockApiResponses();
|
||||||
|
const result = await load({ fetch: vi.fn() } as any);
|
||||||
|
|
||||||
|
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
||||||
|
expect(oele.ingredients.find((i: any) => i.name === 'Olivenöl').isStaple).toBe(true);
|
||||||
|
expect(oele.ingredients.find((i: any) => i.name === 'Butter').isStaple).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('categories without ingredients are included with empty array', async () => {
|
||||||
|
mockGet.mockImplementation((path: string) => {
|
||||||
|
if (path === '/v1/ingredient-categories') {
|
||||||
|
return Promise.resolve({ data: [...mockCategories, { id: 'cat-3', name: 'Leer' }], error: undefined });
|
||||||
|
}
|
||||||
|
if (path === '/v1/ingredients') {
|
||||||
|
return Promise.resolve({ data: mockIngredients, error: undefined });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await load({ fetch: vi.fn() } as any);
|
||||||
|
const leer = result.categories.find((c: any) => c.id === 'cat-3');
|
||||||
|
expect(leer).toBeDefined();
|
||||||
|
expect(leer.ingredients).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user