feat(settings): implement /settings hub page (E1) — Kachel-Ansicht #55

Merged
marcel merged 16 commits from feat/issue-49-settings-kachel-hub into master 2026-04-10 17:39:42 +02:00
2 changed files with 125 additions and 0 deletions
Showing only changes of commit 33cccd3d63 - Show all commits

View File

@@ -0,0 +1,20 @@
import type { PageServerLoad } from './$types';
import { apiClient } from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, locals }) => {
const api = apiClient(fetch);
const [ingredientsRes, householdRes] = await Promise.all([
api.GET('/v1/ingredients'),
api.GET('/v1/households/mine')
]);
const stapleCount = ingredientsRes.data?.filter((i) => i.isStaple).length ?? 0;
const memberCount = householdRes.data?.data?.members?.length ?? 0;
return {
stapleCount,
memberCount,
userName: locals.benutzer!.name
};
};

View File

@@ -0,0 +1,105 @@
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 mockIngredients = [
{ id: 'ing-1', name: 'Olivenöl', isStaple: true },
{ id: 'ing-2', name: 'Butter', isStaple: false },
{ id: 'ing-3', name: 'Salz', isStaple: true }
];
const mockHousehold = {
status: 'OK',
data: {
id: 'hh-1',
name: 'Familie Raddatz',
members: [
{ userId: 'u-1', name: 'Marcel' },
{ userId: 'u-2', name: 'Anna' },
{ userId: 'u-3', name: 'Ben' }
]
}
};
const mockLocals = { benutzer: { id: 'u-1', name: 'Marcel Raddatz' } };
describe('settings 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/ingredients') {
return Promise.resolve({ data: mockIngredients, error: undefined });
}
if (path === '/v1/households/mine') {
return Promise.resolve({ data: mockHousehold, error: undefined });
}
});
}
it('returns stapleCount as number of ingredients where isStaple=true', async () => {
mockApiResponses();
const result = await load({ fetch: vi.fn(), locals: mockLocals } as any);
expect(result.stapleCount).toBe(2);
});
it('returns memberCount as number of household members', async () => {
mockApiResponses();
const result = await load({ fetch: vi.fn(), locals: mockLocals } as any);
expect(result.memberCount).toBe(3);
});
it('returns userName from locals.benutzer.name', async () => {
mockApiResponses();
const result = await load({ fetch: vi.fn(), locals: mockLocals } as any);
expect(result.userName).toBe('Marcel Raddatz');
});
it('fetches ingredients and household in parallel', async () => {
mockApiResponses();
await load({ fetch: vi.fn(), locals: mockLocals } as any);
const calls = mockGet.mock.calls.map((c) => c[0]);
expect(calls).toContain('/v1/ingredients');
expect(calls).toContain('/v1/households/mine');
});
it('defaults stapleCount to 0 when ingredients API fails', async () => {
mockGet.mockImplementation((path: string) => {
if (path === '/v1/ingredients') {
return Promise.resolve({ data: undefined, error: { status: 500 } });
}
if (path === '/v1/households/mine') {
return Promise.resolve({ data: mockHousehold, error: undefined });
}
});
const result = await load({ fetch: vi.fn(), locals: mockLocals } as any);
expect(result.stapleCount).toBe(0);
});
it('defaults memberCount to 0 when household API fails', async () => {
mockGet.mockImplementation((path: string) => {
if (path === '/v1/ingredients') {
return Promise.resolve({ data: mockIngredients, error: undefined });
}
if (path === '/v1/households/mine') {
return Promise.resolve({ data: undefined, error: { status: 500 } });
}
});
const result = await load({ fetch: vi.fn(), locals: mockLocals } as any);
expect(result.memberCount).toBe(0);
});
});