From 7bdc049461f4d08417a5015ec1c19bec91ea4179 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:06:32 +0200 Subject: [PATCH 01/17] feat(staples): add StapleChip component with aria-pressed toggle and focus ring Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/onboarding/StapleChip.svelte | 20 +++++++++ .../src/lib/onboarding/StapleChip.test.ts | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 frontend/src/lib/onboarding/StapleChip.svelte create mode 100644 frontend/src/lib/onboarding/StapleChip.test.ts diff --git a/frontend/src/lib/onboarding/StapleChip.svelte b/frontend/src/lib/onboarding/StapleChip.svelte new file mode 100644 index 0000000..00d5848 --- /dev/null +++ b/frontend/src/lib/onboarding/StapleChip.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/onboarding/StapleChip.test.ts b/frontend/src/lib/onboarding/StapleChip.test.ts new file mode 100644 index 0000000..cc47d93 --- /dev/null +++ b/frontend/src/lib/onboarding/StapleChip.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; +import StapleChip from './StapleChip.svelte'; + +describe('StapleChip', () => { + it('renders a button with the ingredient name', () => { + render(StapleChip, { props: { name: 'Olivenöl', selected: false, onToggle: vi.fn() } }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toBeInTheDocument(); + }); + + it('is aria-pressed="false" when unselected', () => { + render(StapleChip, { props: { name: 'Olivenöl', selected: false, onToggle: vi.fn() } }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'false'); + }); + + it('is aria-pressed="true" when selected', () => { + render(StapleChip, { props: { name: 'Olivenöl', selected: true, onToggle: vi.fn() } }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true'); + }); + + it('calls onToggle with true when unselected chip is clicked', async () => { + const user = userEvent.setup(); + const onToggle = vi.fn(); + render(StapleChip, { props: { name: 'Olivenöl', selected: false, onToggle } }); + + await user.click(screen.getByRole('button', { name: 'Olivenöl' })); + expect(onToggle).toHaveBeenCalledWith(true); + }); + + it('calls onToggle with false when selected chip is clicked', async () => { + const user = userEvent.setup(); + const onToggle = vi.fn(); + render(StapleChip, { props: { name: 'Olivenöl', selected: true, onToggle } }); + + await user.click(screen.getByRole('button', { name: 'Olivenöl' })); + expect(onToggle).toHaveBeenCalledWith(false); + }); + + it('has a visible focus ring class for keyboard accessibility', () => { + render(StapleChip, { props: { name: 'Olivenöl', selected: false, onToggle: vi.fn() } }); + const btn = screen.getByRole('button', { name: 'Olivenöl' }); + expect(btn.className).toContain('focus-visible:outline'); + }); +}); From 376dc036467566b0698c12d1edc2ee7230044ec3 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:07:51 +0200 Subject: [PATCH 02/17] feat(staples): add CategorySection component with eyebrow heading and chip row Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/onboarding/CategorySection.svelte | 26 +++++++++ .../lib/onboarding/CategorySection.test.ts | 55 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 frontend/src/lib/onboarding/CategorySection.svelte create mode 100644 frontend/src/lib/onboarding/CategorySection.test.ts diff --git a/frontend/src/lib/onboarding/CategorySection.svelte b/frontend/src/lib/onboarding/CategorySection.svelte new file mode 100644 index 0000000..d9e4f4d --- /dev/null +++ b/frontend/src/lib/onboarding/CategorySection.svelte @@ -0,0 +1,26 @@ + + +
+

+ {name} +

+
+ {#each ingredients as ingredient (ingredient.id)} + onToggle(ingredient.id, value)} + /> + {/each} +
+
diff --git a/frontend/src/lib/onboarding/CategorySection.test.ts b/frontend/src/lib/onboarding/CategorySection.test.ts new file mode 100644 index 0000000..b37579f --- /dev/null +++ b/frontend/src/lib/onboarding/CategorySection.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import CategorySection from './CategorySection.svelte'; + +const mockIngredients = [ + { id: '1', name: 'Olivenöl', isStaple: true }, + { id: '2', name: 'Butter', isStaple: false }, + { id: '3', name: 'Kokosöl', isStaple: false } +]; + +describe('CategorySection', () => { + it('renders the category name as a heading', () => { + render(CategorySection, { + props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() } + }); + expect(screen.getByText('Öle & Fette')).toBeInTheDocument(); + }); + + it('renders a chip for each ingredient', () => { + render(CategorySection, { + props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() } + }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Butter' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Kokosöl' })).toBeInTheDocument(); + }); + + it('reflects isStaple state on each chip', () => { + render(CategorySection, { + props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() } + }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByRole('button', { name: 'Butter' })).toHaveAttribute('aria-pressed', 'false'); + }); + + it('calls onToggle with ingredient id and new value when chip is clicked', async () => { + const { userEvent } = await import('@testing-library/user-event'); + const user = userEvent.setup(); + const onToggle = vi.fn(); + render(CategorySection, { + props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle } + }); + + await user.click(screen.getByRole('button', { name: 'Butter' })); + expect(onToggle).toHaveBeenCalledWith('2', true); + }); + + it('renders an empty category without crashing', () => { + render(CategorySection, { + props: { name: 'Leer', ingredients: [], onToggle: vi.fn() } + }); + expect(screen.getByText('Leer')).toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); +}); From d577e0231c442269c3c531c0ce94bb95eda80da6 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:11:51 +0200 Subject: [PATCH 03/17] feat(staples): add StaplesManager with optimistic toggle and debounced PATCH Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/onboarding/StaplesManager.svelte | 80 ++++++++++++++ .../src/lib/onboarding/StaplesManager.test.ts | 102 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 frontend/src/lib/onboarding/StaplesManager.svelte create mode 100644 frontend/src/lib/onboarding/StaplesManager.test.ts diff --git a/frontend/src/lib/onboarding/StaplesManager.svelte b/frontend/src/lib/onboarding/StaplesManager.svelte new file mode 100644 index 0000000..3e133ad --- /dev/null +++ b/frontend/src/lib/onboarding/StaplesManager.svelte @@ -0,0 +1,80 @@ + + +
+ {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ {#each categories as category (category.id)} + ({ + ...ing, + isStaple: stapleState[ing.id] ?? ing.isStaple + }))} + onToggle={handleToggle} + /> + {/each} +
+
diff --git a/frontend/src/lib/onboarding/StaplesManager.test.ts b/frontend/src/lib/onboarding/StaplesManager.test.ts new file mode 100644 index 0000000..2dfee3a --- /dev/null +++ b/frontend/src/lib/onboarding/StaplesManager.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; +import StaplesManager from './StaplesManager.svelte'; + +const mockCategories = [ + { + id: 'cat-1', + name: 'Öle & Fette', + ingredients: [ + { id: 'ing-1', name: 'Olivenöl', isStaple: true }, + { id: 'ing-2', name: 'Butter', isStaple: false } + ] + }, + { + id: 'cat-2', + name: 'Gewürze', + ingredients: [ + { id: 'ing-3', name: 'Salz', isStaple: true }, + { id: 'ing-4', name: 'Pfeffer', isStaple: true } + ] + } +]; + +describe('StaplesManager', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true })); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it('renders all categories', () => { + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + expect(screen.getByText('Öle & Fette')).toBeInTheDocument(); + expect(screen.getByText('Gewürze')).toBeInTheDocument(); + }); + + it('renders all chips with correct initial aria-pressed state', () => { + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByRole('button', { name: 'Butter' })).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByRole('button', { name: 'Salz' })).toHaveAttribute('aria-pressed', 'true'); + }); + + it('clicking a chip immediately updates aria-pressed (optimistic)', async () => { + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + + const butter = screen.getByRole('button', { name: 'Butter' }); + expect(butter).toHaveAttribute('aria-pressed', 'false'); + await user.click(butter); + expect(butter).toHaveAttribute('aria-pressed', 'true'); + }); + + it('rapid clicks on same chip result in exactly one fetch call after debounce', async () => { + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + + const butter = screen.getByRole('button', { name: 'Butter' }); + await user.click(butter); + await user.click(butter); + await user.click(butter); + + expect(fetch).not.toHaveBeenCalled(); + vi.advanceTimersByTime(300); + await vi.runAllTimersAsync(); + + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it('reverts chip and shows error when PATCH fails', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false })); + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + + const butter = screen.getByRole('button', { name: 'Butter' }); + await user.click(butter); + expect(butter).toHaveAttribute('aria-pressed', 'true'); + + vi.advanceTimersByTime(300); + await vi.runAllTimersAsync(); + + expect(butter).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByText(/konnte nicht gespeichert werden/i)).toBeInTheDocument(); + }); + + it('uses 2-column grid class in onboarding context', () => { + render(StaplesManager, { props: { categories: mockCategories, context: 'onboarding' } }); + const grid = screen.getByTestId('category-grid'); + expect(grid.className).toContain('md:grid-cols-2'); + }); + + it('uses 3-column grid class in settings context', () => { + render(StaplesManager, { props: { categories: mockCategories, context: 'settings' } }); + const grid = screen.getByTestId('category-grid'); + expect(grid.className).toContain('md:grid-cols-3'); + }); +}); From 54df70a44296ff75d4d024a9918cecd58dafb4aa Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:13:02 +0200 Subject: [PATCH 04/17] feat(staples): add PATCH proxy server route for ingredient staple toggle Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/household/staples/+server.ts | 24 +++++++ .../routes/household/staples/server.test.ts | 63 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 frontend/src/routes/household/staples/+server.ts create mode 100644 frontend/src/routes/household/staples/server.test.ts diff --git a/frontend/src/routes/household/staples/+server.ts b/frontend/src/routes/household/staples/+server.ts new file mode 100644 index 0000000..6c5eab4 --- /dev/null +++ b/frontend/src/routes/household/staples/+server.ts @@ -0,0 +1,24 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { apiClient } from '$lib/server/api'; + +export const PATCH: RequestHandler = async ({ request, fetch }) => { + const body = await request.json(); + const { id, isStaple } = body; + + if (!id) { + return json({ error: 'id is required' }, { status: 400 }); + } + + const api = apiClient(fetch); + const { error } = await api.PATCH('/v1/ingredients/{id}', { + params: { path: { id } }, + body: { isStaple } + }); + + if (error) { + return json({ error: 'Failed to update ingredient' }, { status: 500 }); + } + + return new Response(null, { status: 204 }); +}; diff --git a/frontend/src/routes/household/staples/server.test.ts b/frontend/src/routes/household/staples/server.test.ts new file mode 100644 index 0000000..36d7045 --- /dev/null +++ b/frontend/src/routes/household/staples/server.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('$env/dynamic/private', () => ({ + env: { BACKEND_URL: 'http://localhost:8080' } +})); + +const mockPatch = vi.fn(); +vi.mock('$lib/server/api', () => ({ + apiClient: () => ({ PATCH: mockPatch }) +})); + +describe('household staples PATCH handler', () => { + let PATCH: any; + + beforeEach(async () => { + mockPatch.mockReset(); + const mod = await import('./+server'); + PATCH = mod.PATCH; + }); + + function createRequest(body: object) { + return { + request: { + json: () => Promise.resolve(body) + }, + fetch: vi.fn() + } as any; + } + + it('calls backend PATCH /v1/ingredients/{id} with isStaple', async () => { + mockPatch.mockResolvedValue({ data: {}, error: undefined }); + + await PATCH(createRequest({ id: 'ing-1', isStaple: true })); + + expect(mockPatch).toHaveBeenCalledWith('/v1/ingredients/{id}', { + params: { path: { id: 'ing-1' } }, + body: { isStaple: true } + }); + }); + + it('returns 204 on success', async () => { + mockPatch.mockResolvedValue({ data: {}, error: undefined }); + + const response = await PATCH(createRequest({ id: 'ing-1', isStaple: true })); + + expect(response.status).toBe(204); + }); + + it('returns 500 when backend returns an error', async () => { + mockPatch.mockResolvedValue({ data: undefined, error: { status: 500, message: 'error' } }); + + const response = await PATCH(createRequest({ id: 'ing-1', isStaple: false })); + + expect(response.status).toBe(500); + }); + + it('returns 400 when id is missing', async () => { + const response = await PATCH(createRequest({ isStaple: true })); + + expect(response.status).toBe(400); + expect(mockPatch).not.toHaveBeenCalled(); + }); +}); From 3550d681dc79a5b851c0ad1b1e697d5c23a12473 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:14:25 +0200 Subject: [PATCH 05/17] feat(staples): load categories and ingredients, group by category Co-Authored-By: Claude Sonnet 4.6 --- .../routes/household/staples/+page.server.ts | 28 ++++++ .../household/staples/page.server.test.ts | 87 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 frontend/src/routes/household/staples/+page.server.ts create mode 100644 frontend/src/routes/household/staples/page.server.test.ts diff --git a/frontend/src/routes/household/staples/+page.server.ts b/frontend/src/routes/household/staples/+page.server.ts new file mode 100644 index 0000000..87e6b5a --- /dev/null +++ b/frontend/src/routes/household/staples/+page.server.ts @@ -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 }; +}; diff --git a/frontend/src/routes/household/staples/page.server.test.ts b/frontend/src/routes/household/staples/page.server.test.ts new file mode 100644 index 0000000..6b28f30 --- /dev/null +++ b/frontend/src/routes/household/staples/page.server.test.ts @@ -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); + }); +}); From 97175e7d9d8ca5ca5dfdc4561bedaec57fa1a3b1 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:16:08 +0200 Subject: [PATCH 06/17] feat(staples): add staples page with onboarding and settings layouts Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/household/staples/+page.svelte | 44 +++++++++- .../src/routes/household/staples/page.test.ts | 86 +++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 frontend/src/routes/household/staples/page.test.ts diff --git a/frontend/src/routes/household/staples/+page.svelte b/frontend/src/routes/household/staples/+page.svelte index 6a67101..3aec917 100644 --- a/frontend/src/routes/household/staples/+page.svelte +++ b/frontend/src/routes/household/staples/+page.svelte @@ -2,6 +2,44 @@ Vorräte einrichten — Mealplan -
-

A3 — Vorräte einrichten (coming soon)

-
+ + +{#if isOnboarding} +
+ + + + +
+ +

Schritt 2 von 3

+ + +
+ +
+ + + +
+
+{:else} +
+

Vorräte

+ +
+{/if} diff --git a/frontend/src/routes/household/staples/page.test.ts b/frontend/src/routes/household/staples/page.test.ts new file mode 100644 index 0000000..3f016b5 --- /dev/null +++ b/frontend/src/routes/household/staples/page.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import Page from './+page.svelte'; + +vi.mock('$app/state', () => ({ + page: { url: { searchParams: { get: vi.fn() } } } +})); + +const mockCategories = [ + { + id: 'cat-1', + name: 'Öle & Fette', + ingredients: [ + { id: 'ing-1', name: 'Olivenöl', isStaple: true }, + { id: 'ing-2', name: 'Butter', isStaple: false } + ] + } +]; + +describe('staples page — onboarding context (?ctx=onboarding)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true })); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders ProgressSidebar with step 2 active', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step'); + }); + + it('renders Continue button linking to /household/invite', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + const continueLink = screen.getByRole('link', { name: /weiter/i }); + expect(continueLink).toHaveAttribute('href', '/household/invite'); + }); + + it('renders Skip button linking to /planner', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + const skipLink = screen.getByRole('link', { name: /überspringen/i }); + expect(skipLink).toHaveAttribute('href', '/planner'); + }); + + it('renders the StaplesManager with categories', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + expect(screen.getByText('Öle & Fette')).toBeInTheDocument(); + }); + + it('sets the page title', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + expect(document.title).toBe('Vorräte einrichten — Mealplan'); + }); + + it('renders mobile step indicator Schritt 2 von 3', () => { + render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument(); + }); +}); + +describe('staples page — settings context (no ctx)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true })); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('does not render ProgressSidebar', () => { + render(Page, { props: { data: { categories: mockCategories } } }); + expect(screen.queryByTestId('step-1')).not.toBeInTheDocument(); + }); + + it('does not render Continue or Skip buttons', () => { + render(Page, { props: { data: { categories: mockCategories } } }); + expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument(); + }); + + it('renders a settings heading', () => { + render(Page, { props: { data: { categories: mockCategories } } }); + expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument(); + }); +}); From d68a9d9312b11782d9ce2e119bf7a02b3d972f64 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:16:33 +0200 Subject: [PATCH 07/17] refactor(setup): redirect to /household/staples?ctx=onboarding after household creation Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/setup/+page.server.ts | 2 +- frontend/src/routes/household/setup/page.server.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/household/setup/+page.server.ts b/frontend/src/routes/household/setup/+page.server.ts index 6fe21b8..d780d6c 100644 --- a/frontend/src/routes/household/setup/+page.server.ts +++ b/frontend/src/routes/household/setup/+page.server.ts @@ -34,6 +34,6 @@ export const actions = { }); } - throw redirect(303, '/household/staples'); + throw redirect(303, '/household/staples?ctx=onboarding'); } } satisfies Actions; diff --git a/frontend/src/routes/household/setup/page.server.test.ts b/frontend/src/routes/household/setup/page.server.test.ts index 46e3679..a4a43ab 100644 --- a/frontend/src/routes/household/setup/page.server.test.ts +++ b/frontend/src/routes/household/setup/page.server.test.ts @@ -97,7 +97,7 @@ describe('household setup — form action', () => { expect.unreachable(); } catch (e: any) { expect(e.status).toBe(303); - expect(e.location).toBe('/household/staples'); + expect(e.location).toBe('/household/staples?ctx=onboarding'); } }); From 7979076f5ea140f21fee3f58c11f0d7867cae9a5 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 20:16:46 +0200 Subject: [PATCH 08/17] feat(invite): stub household invite page as onboarding Continue target Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/invite/+page.svelte | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 frontend/src/routes/household/invite/+page.svelte diff --git a/frontend/src/routes/household/invite/+page.svelte b/frontend/src/routes/household/invite/+page.svelte new file mode 100644 index 0000000..a9bf614 --- /dev/null +++ b/frontend/src/routes/household/invite/+page.svelte @@ -0,0 +1,7 @@ + + Mitglieder einladen — Mealplan + + +
+

A4 — Mitglieder einladen (coming soon)

+
From 7b497be1c175f078c4cc4fcb20442316706a7775 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:23:48 +0200 Subject: [PATCH 09/17] test(staples): add empty categories edge case to StaplesManager Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/onboarding/StaplesManager.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/onboarding/StaplesManager.test.ts b/frontend/src/lib/onboarding/StaplesManager.test.ts index 2dfee3a..1f7a96d 100644 --- a/frontend/src/lib/onboarding/StaplesManager.test.ts +++ b/frontend/src/lib/onboarding/StaplesManager.test.ts @@ -99,4 +99,9 @@ describe('StaplesManager', () => { const grid = screen.getByTestId('category-grid'); expect(grid.className).toContain('md:grid-cols-3'); }); + + it('renders without crashing when categories is empty', () => { + render(StaplesManager, { props: { categories: [], context: 'onboarding' } }); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); }); From 65f18cfb43dd22050ce12948fc2033332fc8f07c Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:24:07 +0200 Subject: [PATCH 10/17] test(staples): cover API failure fallback in page load Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/staples/page.server.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/routes/household/staples/page.server.test.ts b/frontend/src/routes/household/staples/page.server.test.ts index 6b28f30..308ac2e 100644 --- a/frontend/src/routes/household/staples/page.server.test.ts +++ b/frontend/src/routes/household/staples/page.server.test.ts @@ -84,4 +84,10 @@ describe('household staples page — load', () => { expect(leer).toBeDefined(); expect(leer.ingredients).toHaveLength(0); }); + + it('returns empty categories when API fails', async () => { + mockGet.mockResolvedValue({ data: undefined, error: { status: 500 } }); + const result = await load({ fetch: vi.fn() } as any); + expect(result.categories).toEqual([]); + }); }); From 21b873b85b064fcf1083d32c77b6361fee8e409e Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:24:35 +0200 Subject: [PATCH 11/17] fix(staples): validate isStaple is boolean before forwarding to backend Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/staples/+server.ts | 4 ++++ .../src/routes/household/staples/server.test.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/frontend/src/routes/household/staples/+server.ts b/frontend/src/routes/household/staples/+server.ts index 6c5eab4..c22cb44 100644 --- a/frontend/src/routes/household/staples/+server.ts +++ b/frontend/src/routes/household/staples/+server.ts @@ -10,6 +10,10 @@ export const PATCH: RequestHandler = async ({ request, fetch }) => { return json({ error: 'id is required' }, { status: 400 }); } + if (typeof isStaple !== 'boolean') { + return json({ error: 'isStaple must be a boolean' }, { status: 400 }); + } + const api = apiClient(fetch); const { error } = await api.PATCH('/v1/ingredients/{id}', { params: { path: { id } }, diff --git a/frontend/src/routes/household/staples/server.test.ts b/frontend/src/routes/household/staples/server.test.ts index 36d7045..81418dc 100644 --- a/frontend/src/routes/household/staples/server.test.ts +++ b/frontend/src/routes/household/staples/server.test.ts @@ -60,4 +60,18 @@ describe('household staples PATCH handler', () => { expect(response.status).toBe(400); expect(mockPatch).not.toHaveBeenCalled(); }); + + it('returns 400 when isStaple is missing', async () => { + const response = await PATCH(createRequest({ id: 'ing-1' })); + + expect(response.status).toBe(400); + expect(mockPatch).not.toHaveBeenCalled(); + }); + + it('returns 400 when isStaple is not a boolean', async () => { + const response = await PATCH(createRequest({ id: 'ing-1', isStaple: 'yes' })); + + expect(response.status).toBe(400); + expect(mockPatch).not.toHaveBeenCalled(); + }); }); From 3581af2bf91d9138b862d2783a3a3b20cabf1a63 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:25:06 +0200 Subject: [PATCH 12/17] fix(staples): forward backend error status code instead of always 500 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/staples/+server.ts | 3 ++- frontend/src/routes/household/staples/server.test.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/household/staples/+server.ts b/frontend/src/routes/household/staples/+server.ts index c22cb44..11ac42f 100644 --- a/frontend/src/routes/household/staples/+server.ts +++ b/frontend/src/routes/household/staples/+server.ts @@ -21,7 +21,8 @@ export const PATCH: RequestHandler = async ({ request, fetch }) => { }); if (error) { - return json({ error: 'Failed to update ingredient' }, { status: 500 }); + const status = (error as { status?: number }).status ?? 500; + return json({ error: 'Failed to update ingredient' }, { status }); } return new Response(null, { status: 204 }); diff --git a/frontend/src/routes/household/staples/server.test.ts b/frontend/src/routes/household/staples/server.test.ts index 81418dc..10b3c04 100644 --- a/frontend/src/routes/household/staples/server.test.ts +++ b/frontend/src/routes/household/staples/server.test.ts @@ -46,7 +46,7 @@ describe('household staples PATCH handler', () => { expect(response.status).toBe(204); }); - it('returns 500 when backend returns an error', async () => { + it('returns 500 when backend returns a 500 error', async () => { mockPatch.mockResolvedValue({ data: undefined, error: { status: 500, message: 'error' } }); const response = await PATCH(createRequest({ id: 'ing-1', isStaple: false })); @@ -54,6 +54,14 @@ describe('household staples PATCH handler', () => { expect(response.status).toBe(500); }); + it('forwards backend 404 status when ingredient not found', async () => { + mockPatch.mockResolvedValue({ data: undefined, error: { status: 404 } }); + + const response = await PATCH(createRequest({ id: 'ing-1', isStaple: false })); + + expect(response.status).toBe(404); + }); + it('returns 400 when id is missing', async () => { const response = await PATCH(createRequest({ isStaple: true })); From 45b7e7b003994a5e0634ab21f7000c11bf2fa661 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:25:40 +0200 Subject: [PATCH 13/17] =?UTF-8?q?fix(staples):=20add=20role=20guard=20?= =?UTF-8?q?=E2=80=94=20only=20planer=20role=20can=20toggle=20staples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/staples/+server.ts | 6 +++++- frontend/src/routes/household/staples/server.test.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/household/staples/+server.ts b/frontend/src/routes/household/staples/+server.ts index 11ac42f..ebc3f3a 100644 --- a/frontend/src/routes/household/staples/+server.ts +++ b/frontend/src/routes/household/staples/+server.ts @@ -2,7 +2,11 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { apiClient } from '$lib/server/api'; -export const PATCH: RequestHandler = async ({ request, fetch }) => { +export const PATCH: RequestHandler = async ({ request, fetch, locals }) => { + if (locals.benutzer?.rolle !== 'planer') { + return json({ error: 'Forbidden' }, { status: 403 }); + } + const body = await request.json(); const { id, isStaple } = body; diff --git a/frontend/src/routes/household/staples/server.test.ts b/frontend/src/routes/household/staples/server.test.ts index 10b3c04..0490cf5 100644 --- a/frontend/src/routes/household/staples/server.test.ts +++ b/frontend/src/routes/household/staples/server.test.ts @@ -18,12 +18,13 @@ describe('household staples PATCH handler', () => { PATCH = mod.PATCH; }); - function createRequest(body: object) { + function createRequest(body: object, rolle: 'planer' | 'mitglied' = 'planer') { return { request: { json: () => Promise.resolve(body) }, - fetch: vi.fn() + fetch: vi.fn(), + locals: { benutzer: { rolle } } } as any; } @@ -76,6 +77,13 @@ describe('household staples PATCH handler', () => { expect(mockPatch).not.toHaveBeenCalled(); }); + it('returns 403 when caller has mitglied role', async () => { + const response = await PATCH(createRequest({ id: 'ing-1', isStaple: true }, 'mitglied')); + + expect(response.status).toBe(403); + expect(mockPatch).not.toHaveBeenCalled(); + }); + it('returns 400 when isStaple is not a boolean', async () => { const response = await PATCH(createRequest({ id: 'ing-1', isStaple: 'yes' })); From 8daaa0e21d183d263f24c6e94e7c0876ebaf5b51 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:27:43 +0200 Subject: [PATCH 14/17] fix(staples): pass ctx from URL through load function; fix script order in page Co-Authored-By: Claude Sonnet 4.6 --- .../routes/household/staples/+page.server.ts | 4 ++-- .../src/routes/household/staples/+page.svelte | 12 +++++----- .../household/staples/page.server.test.ts | 24 +++++++++++++++---- .../src/routes/household/staples/page.test.ts | 22 +++++++---------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/frontend/src/routes/household/staples/+page.server.ts b/frontend/src/routes/household/staples/+page.server.ts index 87e6b5a..0927f77 100644 --- a/frontend/src/routes/household/staples/+page.server.ts +++ b/frontend/src/routes/household/staples/+page.server.ts @@ -1,7 +1,7 @@ import type { PageServerLoad } from './$types'; import { apiClient } from '$lib/server/api'; -export const load: PageServerLoad = async ({ fetch }) => { +export const load: PageServerLoad = async ({ fetch, url }) => { const api = apiClient(fetch); const [categoriesResult, ingredientsResult] = await Promise.all([ @@ -24,5 +24,5 @@ export const load: PageServerLoad = async ({ fetch }) => { })) })); - return { categories }; + return { categories, ctx: url.searchParams.get('ctx') }; }; diff --git a/frontend/src/routes/household/staples/+page.svelte b/frontend/src/routes/household/staples/+page.svelte index 3aec917..aaa3615 100644 --- a/frontend/src/routes/household/staples/+page.svelte +++ b/frontend/src/routes/household/staples/+page.svelte @@ -1,18 +1,18 @@ - - Vorräte einrichten — Mealplan - - + + Vorräte einrichten — Mealplan + + {#if isOnboarding}
diff --git a/frontend/src/routes/household/staples/page.server.test.ts b/frontend/src/routes/household/staples/page.server.test.ts index 308ac2e..85af50a 100644 --- a/frontend/src/routes/household/staples/page.server.test.ts +++ b/frontend/src/routes/household/staples/page.server.test.ts @@ -41,9 +41,23 @@ describe('household staples page — load', () => { }); } + it('passes ctx from url searchParams into returned data', async () => { + mockApiResponses(); + const url = new URL('http://localhost/household/staples?ctx=onboarding'); + const result = await load({ fetch: vi.fn(), url } as any); + expect(result.ctx).toBe('onboarding'); + }); + + it('returns ctx as null when no ctx param is present', async () => { + mockApiResponses(); + const url = new URL('http://localhost/household/staples'); + const result = await load({ fetch: vi.fn(), url } as any); + expect(result.ctx).toBeNull(); + }); + it('fetches both categories and ingredients in parallel', async () => { mockApiResponses(); - await load({ fetch: vi.fn() } as any); + await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any); const calls = mockGet.mock.calls.map((c) => c[0]); expect(calls).toContain('/v1/ingredient-categories'); @@ -52,7 +66,7 @@ describe('household staples page — load', () => { it('groups ingredients by category id', async () => { mockApiResponses(); - const result = await load({ fetch: vi.fn() } as any); + const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any); expect(result.categories).toHaveLength(2); const oele = result.categories.find((c: any) => c.id === 'cat-1'); @@ -62,7 +76,7 @@ describe('household staples page — load', () => { it('preserves isStaple flag on each ingredient', async () => { mockApiResponses(); - const result = await load({ fetch: vi.fn() } as any); + const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } 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); @@ -79,7 +93,7 @@ describe('household staples page — load', () => { } }); - const result = await load({ fetch: vi.fn() } as any); + const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any); const leer = result.categories.find((c: any) => c.id === 'cat-3'); expect(leer).toBeDefined(); expect(leer.ingredients).toHaveLength(0); @@ -87,7 +101,7 @@ describe('household staples page — load', () => { it('returns empty categories when API fails', async () => { mockGet.mockResolvedValue({ data: undefined, error: { status: 500 } }); - const result = await load({ fetch: vi.fn() } as any); + const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any); expect(result.categories).toEqual([]); }); }); diff --git a/frontend/src/routes/household/staples/page.test.ts b/frontend/src/routes/household/staples/page.test.ts index 3f016b5..873bbc9 100644 --- a/frontend/src/routes/household/staples/page.test.ts +++ b/frontend/src/routes/household/staples/page.test.ts @@ -2,10 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import Page from './+page.svelte'; -vi.mock('$app/state', () => ({ - page: { url: { searchParams: { get: vi.fn() } } } -})); - const mockCategories = [ { id: 'cat-1', @@ -27,34 +23,34 @@ describe('staples page — onboarding context (?ctx=onboarding)', () => { }); it('renders ProgressSidebar with step 2 active', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step'); }); it('renders Continue button linking to /household/invite', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); const continueLink = screen.getByRole('link', { name: /weiter/i }); expect(continueLink).toHaveAttribute('href', '/household/invite'); }); it('renders Skip button linking to /planner', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); const skipLink = screen.getByRole('link', { name: /überspringen/i }); expect(skipLink).toHaveAttribute('href', '/planner'); }); it('renders the StaplesManager with categories', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); expect(screen.getByText('Öle & Fette')).toBeInTheDocument(); }); it('sets the page title', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); expect(document.title).toBe('Vorräte einrichten — Mealplan'); }); it('renders mobile step indicator Schritt 2 von 3', () => { - render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } }); + render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } }); expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument(); }); }); @@ -69,18 +65,18 @@ describe('staples page — settings context (no ctx)', () => { }); it('does not render ProgressSidebar', () => { - render(Page, { props: { data: { categories: mockCategories } } }); + render(Page, { props: { data: { categories: mockCategories, ctx: null } } }); expect(screen.queryByTestId('step-1')).not.toBeInTheDocument(); }); it('does not render Continue or Skip buttons', () => { - render(Page, { props: { data: { categories: mockCategories } } }); + render(Page, { props: { data: { categories: mockCategories, ctx: null } } }); expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument(); }); it('renders a settings heading', () => { - render(Page, { props: { data: { categories: mockCategories } } }); + render(Page, { props: { data: { categories: mockCategories, ctx: null } } }); expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument(); }); }); From 73b33ee956d9e2bb8e69b45629b5cdcb33d86d93 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:28:50 +0200 Subject: [PATCH 15/17] fix(staples): apply design-system button spec to StapleChip (13px, tracking, font-sans) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/onboarding/StapleChip.svelte | 2 +- frontend/src/lib/onboarding/StapleChip.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/onboarding/StapleChip.svelte b/frontend/src/lib/onboarding/StapleChip.svelte index 00d5848..71ddb85 100644 --- a/frontend/src/lib/onboarding/StapleChip.svelte +++ b/frontend/src/lib/onboarding/StapleChip.svelte @@ -10,7 +10,7 @@ type="button" aria-pressed={selected} onclick={() => onToggle(!selected)} - class="inline-flex text-[12px] font-medium px-[12px] py-[6px] rounded-full border cursor-pointer + class="inline-flex font-sans text-[13px] font-medium tracking-[0.04em] px-[12px] py-[6px] rounded-full border cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--green-light)] {selected ? 'bg-[var(--green-tint)] border-[var(--green-light)] text-[var(--green-dark)]' diff --git a/frontend/src/lib/onboarding/StapleChip.test.ts b/frontend/src/lib/onboarding/StapleChip.test.ts index cc47d93..f9e6939 100644 --- a/frontend/src/lib/onboarding/StapleChip.test.ts +++ b/frontend/src/lib/onboarding/StapleChip.test.ts @@ -42,4 +42,12 @@ describe('StapleChip', () => { const btn = screen.getByRole('button', { name: 'Olivenöl' }); expect(btn.className).toContain('focus-visible:outline'); }); + + it('uses design-system button text spec: 13px, tracking, font-sans', () => { + render(StapleChip, { props: { name: 'Olivenöl', selected: false, onToggle: vi.fn() } }); + const btn = screen.getByRole('button', { name: 'Olivenöl' }); + expect(btn.className).toContain('text-[13px]'); + expect(btn.className).toContain('tracking-[0.04em]'); + expect(btn.className).toContain('font-sans'); + }); }); From 2d6ddf0e4835a8af8f7b2f692bc40ab9fd7c7e2a Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:29:53 +0200 Subject: [PATCH 16/17] fix(staples): apply design-system styles to nav links and settings heading Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/household/staples/+page.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/household/staples/+page.svelte b/frontend/src/routes/household/staples/+page.svelte index aaa3615..5db2188 100644 --- a/frontend/src/routes/household/staples/+page.svelte +++ b/frontend/src/routes/household/staples/+page.svelte @@ -32,14 +32,20 @@
{:else}
-

Vorräte

+

Vorräte

{/if} From df954620949c45a60ce08995a11b18fd7a65a878 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:30:19 +0200 Subject: [PATCH 17/17] refactor(staples): convert dynamic userEvent import to static in CategorySection test Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/onboarding/CategorySection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/onboarding/CategorySection.test.ts b/frontend/src/lib/onboarding/CategorySection.test.ts index b37579f..46c0347 100644 --- a/frontend/src/lib/onboarding/CategorySection.test.ts +++ b/frontend/src/lib/onboarding/CategorySection.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; import CategorySection from './CategorySection.svelte'; const mockIngredients = [ @@ -34,7 +35,6 @@ describe('CategorySection', () => { }); it('calls onToggle with ingredient id and new value when chip is clicked', async () => { - const { userEvent } = await import('@testing-library/user-event'); const user = userEvent.setup(); const onToggle = vi.fn(); render(CategorySection, {