From 86a25eb038e87a39f4f5cb5811412831f8b29cf0 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:56:35 +0200 Subject: [PATCH 1/6] feat(recipes): add RecipeHero component with image/no-image variants Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/recipes/RecipeHero.svelte | 67 +++++++++++++++++++++ frontend/src/lib/recipes/RecipeHero.test.ts | 66 ++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 frontend/src/lib/recipes/RecipeHero.svelte create mode 100644 frontend/src/lib/recipes/RecipeHero.test.ts diff --git a/frontend/src/lib/recipes/RecipeHero.svelte b/frontend/src/lib/recipes/RecipeHero.svelte new file mode 100644 index 0000000..9674f6c --- /dev/null +++ b/frontend/src/lib/recipes/RecipeHero.svelte @@ -0,0 +1,67 @@ + + +
+ {#if hasImage} + {recipe.name} +
+ {/if} + +
+ ← Zurück + +

+ {recipe.name} +

+ +
+ {#if recipe.cookTimeMin != null} + {recipe.cookTimeMin} Min + {/if} + {#if recipe.effort} + {recipe.effort} + {/if} + {#if recipe.serves != null} + {recipe.serves} Port. + {/if} +
+ + Jetzt kochen +
+
diff --git a/frontend/src/lib/recipes/RecipeHero.test.ts b/frontend/src/lib/recipes/RecipeHero.test.ts new file mode 100644 index 0000000..54c4d40 --- /dev/null +++ b/frontend/src/lib/recipes/RecipeHero.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import RecipeHero from './RecipeHero.svelte'; + +const baseRecipe = { + id: 'r1', + name: 'Spaghetti Bolognese', + serves: 4, + cookTimeMin: 30, + effort: 'Easy', + heroImageUrl: undefined as string | undefined, + tags: [] as { id: string; name: string; tagType?: string }[] +}; + +describe('RecipeHero', () => { + it('renders the recipe name', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + expect(screen.getByText('Spaghetti Bolognese')).toBeInTheDocument(); + }); + + it('renders green-tint hero when no image', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + const hero = document.querySelector('[data-testid="recipe-hero"]'); + expect(hero?.className).toContain('bg-[var(--green-tint)]'); + }); + + it('renders image when heroImageUrl is provided', () => { + render(RecipeHero, { + props: { recipe: { ...baseRecipe, heroImageUrl: '/uploads/pasta.jpg' } } + }); + const img = screen.getByRole('img', { name: /spaghetti bolognese/i }); + expect(img).toHaveAttribute('src', '/uploads/pasta.jpg'); + }); + + it('renders cook time pill', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + expect(screen.getByText(/30 Min/)).toBeInTheDocument(); + }); + + it('renders effort pill', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + expect(screen.getByText(/Easy/)).toBeInTheDocument(); + }); + + it('renders serves pill', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + expect(screen.getByText(/4/)).toBeInTheDocument(); + }); + + it('renders back link to /recipes', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + const backLink = screen.getByRole('link', { name: /zurück/i }); + expect(backLink).toHaveAttribute('href', '/recipes'); + }); + + it('renders cook now link to /cook/[id]', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + const cookLink = screen.getByRole('link', { name: /jetzt kochen/i }); + expect(cookLink).toHaveAttribute('href', '/cook/r1'); + }); + + it('does not render img when no heroImageUrl', () => { + render(RecipeHero, { props: { recipe: baseRecipe } }); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + }); +}); From c7e56a173d92049524e2385d8a5bd110859b0ea2 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:57:36 +0200 Subject: [PATCH 2/6] feat(recipes): add IngredientList component (read-only) Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/recipes/IngredientList.svelte | 32 ++++++++++++++ .../src/lib/recipes/IngredientList.test.ts | 44 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 frontend/src/lib/recipes/IngredientList.svelte create mode 100644 frontend/src/lib/recipes/IngredientList.test.ts diff --git a/frontend/src/lib/recipes/IngredientList.svelte b/frontend/src/lib/recipes/IngredientList.svelte new file mode 100644 index 0000000..3a15099 --- /dev/null +++ b/frontend/src/lib/recipes/IngredientList.svelte @@ -0,0 +1,32 @@ + + +
+

+ Zutaten +

+ +
    + {#each ingredients as ingredient (ingredient.ingredientId ?? ingredient.name)} +
  • + {#if ingredient.quantity != null} + + {ingredient.quantity}{ingredient.unit != null ? ` ${ingredient.unit}` : ''} + + {/if} + {ingredient.name} +
  • + {/each} +
+
diff --git a/frontend/src/lib/recipes/IngredientList.test.ts b/frontend/src/lib/recipes/IngredientList.test.ts new file mode 100644 index 0000000..927348a --- /dev/null +++ b/frontend/src/lib/recipes/IngredientList.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import IngredientList from './IngredientList.svelte'; + +const mockIngredients = [ + { ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' }, + { ingredientId: 'i2', name: 'Hackfleisch', quantity: 400, unit: 'g' }, + { ingredientId: 'i3', name: 'Salz', quantity: undefined, unit: undefined } +]; + +describe('IngredientList', () => { + it('renders the section heading', () => { + render(IngredientList, { props: { ingredients: mockIngredients } }); + expect(screen.getByText(/zutaten/i)).toBeInTheDocument(); + }); + + it('renders a row for each ingredient', () => { + render(IngredientList, { props: { ingredients: mockIngredients } }); + expect(screen.getByText('Spaghetti')).toBeInTheDocument(); + expect(screen.getByText('Hackfleisch')).toBeInTheDocument(); + expect(screen.getByText('Salz')).toBeInTheDocument(); + }); + + it('renders quantity and unit when present', () => { + render(IngredientList, { props: { ingredients: mockIngredients } }); + expect(screen.getByText('200 g')).toBeInTheDocument(); + expect(screen.getByText('400 g')).toBeInTheDocument(); + }); + + it('renders no quantity when not present', () => { + render(IngredientList, { props: { ingredients: mockIngredients } }); + expect(screen.queryByText('undefined')).not.toBeInTheDocument(); + }); + + it('has no remove buttons (read-only)', () => { + render(IngredientList, { props: { ingredients: mockIngredients } }); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders empty state when ingredients array is empty', () => { + render(IngredientList, { props: { ingredients: [] } }); + expect(screen.getByText(/zutaten/i)).toBeInTheDocument(); + }); +}); From b39d04accefdbc0005c1bc4637425be9a742bfc8 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:58:39 +0200 Subject: [PATCH 3/6] feat(recipes): add StepList component with numbered circles Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/recipes/StepList.svelte | 32 +++++++++++++++ frontend/src/lib/recipes/StepList.test.ts | 50 +++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 frontend/src/lib/recipes/StepList.svelte create mode 100644 frontend/src/lib/recipes/StepList.test.ts diff --git a/frontend/src/lib/recipes/StepList.svelte b/frontend/src/lib/recipes/StepList.svelte new file mode 100644 index 0000000..7c7f104 --- /dev/null +++ b/frontend/src/lib/recipes/StepList.svelte @@ -0,0 +1,32 @@ + + +
+

+ Zubereitung +

+
    + {#each sortedSteps as step (step.stepNumber)} +
  1. +
    + {step.stepNumber} +
    +

    + {step.instruction} +

    +
  2. + {/each} +
+
diff --git a/frontend/src/lib/recipes/StepList.test.ts b/frontend/src/lib/recipes/StepList.test.ts new file mode 100644 index 0000000..5c8697b --- /dev/null +++ b/frontend/src/lib/recipes/StepList.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import StepList from './StepList.svelte'; + +const mockSteps = [ + { stepNumber: 1, instruction: 'Wasser zum Kochen bringen' }, + { stepNumber: 2, instruction: 'Spaghetti al dente kochen' }, + { stepNumber: 3, instruction: 'Sauce bereiten' } +]; + +describe('StepList', () => { + it('renders the section heading', () => { + render(StepList, { props: { steps: mockSteps } }); + expect(screen.getByText(/zubereitung/i)).toBeInTheDocument(); + }); + + it('renders each step instruction', () => { + render(StepList, { props: { steps: mockSteps } }); + expect(screen.getByText('Wasser zum Kochen bringen')).toBeInTheDocument(); + expect(screen.getByText('Spaghetti al dente kochen')).toBeInTheDocument(); + expect(screen.getByText('Sauce bereiten')).toBeInTheDocument(); + }); + + it('renders step numbers', () => { + render(StepList, { props: { steps: mockSteps } }); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + it('renders numbered circles with step numbers', () => { + render(StepList, { props: { steps: mockSteps } }); + const circles = document.querySelectorAll('[data-testid="step-circle"]'); + expect(circles).toHaveLength(3); + }); + + it('renders steps in stepNumber order', () => { + const shuffled = [mockSteps[2], mockSteps[0], mockSteps[1]]; + render(StepList, { props: { steps: shuffled } }); + const circles = document.querySelectorAll('[data-testid="step-circle"]'); + expect(circles[0].textContent).toBe('1'); + expect(circles[1].textContent).toBe('2'); + expect(circles[2].textContent).toBe('3'); + }); + + it('renders empty state when no steps', () => { + render(StepList, { props: { steps: [] } }); + expect(screen.getByText(/zubereitung/i)).toBeInTheDocument(); + }); +}); From ce860d68e493bfe1a1863618673aa29b5d817ce1 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 10:00:02 +0200 Subject: [PATCH 4/6] feat(recipes): add recipe detail load function with 404 handling Co-Authored-By: Claude Sonnet 4.6 --- .../routes/(app)/recipes/[id]/+page.server.ts | 41 +++++++++++++ .../(app)/recipes/[id]/page.server.test.ts | 57 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 frontend/src/routes/(app)/recipes/[id]/+page.server.ts create mode 100644 frontend/src/routes/(app)/recipes/[id]/page.server.test.ts diff --git a/frontend/src/routes/(app)/recipes/[id]/+page.server.ts b/frontend/src/routes/(app)/recipes/[id]/+page.server.ts new file mode 100644 index 0000000..400a297 --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/+page.server.ts @@ -0,0 +1,41 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { apiClient } from '$lib/server/api'; + +export const load: PageServerLoad = async ({ fetch, params }) => { + const api = apiClient(fetch); + const { data, error: apiError } = await api.GET('/v1/recipes/{id}', { + params: { path: { id: params.id } } + }); + + if (apiError || !data) { + error(404, 'Recipe not found'); + } + + return { + recipe: { + id: data.id!, + name: data.name!, + serves: data.serves, + cookTimeMin: data.cookTimeMin, + effort: data.effort, + heroImageUrl: data.heroImageUrl, + ingredients: (data.ingredients ?? []).map((ing) => ({ + ingredientId: ing.ingredientId, + name: ing.name, + quantity: ing.quantity, + unit: ing.unit, + sortOrder: ing.sortOrder + })), + steps: (data.steps ?? []).map((s) => ({ + stepNumber: s.stepNumber, + instruction: s.instruction + })), + tags: (data.tags ?? []).map((t) => ({ + id: t.id!, + name: t.name!, + tagType: t.tagType + })) + } + }; +}; diff --git a/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts new file mode 100644 index 0000000..a5dc7bb --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts @@ -0,0 +1,57 @@ +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 }) +})); + +describe('recipe detail page — load', () => { + let load: any; + + beforeEach(async () => { + mockGet.mockReset(); + vi.resetModules(); + const mod = await import('./+page.server'); + load = mod.load; + }); + + const mockRecipe = { + id: 'r1', + name: 'Spaghetti Bolognese', + serves: 4, + cookTimeMin: 30, + effort: 'Easy', + heroImageUrl: undefined, + ingredients: [ + { ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' } + ], + steps: [{ stepNumber: 1, instruction: 'Kochen' }], + tags: [] + }; + + it('fetches recipe from GET /v1/recipes/{id}', async () => { + mockGet.mockResolvedValue({ data: mockRecipe, error: undefined }); + await load({ fetch: vi.fn(), params: { id: 'r1' } } as any); + expect(mockGet).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({ + params: { path: { id: 'r1' } } + })); + }); + + it('returns recipe data on success', async () => { + mockGet.mockResolvedValue({ data: mockRecipe, error: undefined }); + const result = await load({ fetch: vi.fn(), params: { id: 'r1' } } as any); + expect(result.recipe.name).toBe('Spaghetti Bolognese'); + expect(result.recipe.serves).toBe(4); + }); + + it('throws 404 error when API returns error', async () => { + mockGet.mockResolvedValue({ data: undefined, error: { status: 404 } }); + await expect( + load({ fetch: vi.fn(), params: { id: 'nonexistent' } } as any) + ).rejects.toMatchObject({ status: 404 }); + }); +}); From 00c48a7c969c93701fcabf68b379f830eb36cbe7 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 10:02:20 +0200 Subject: [PATCH 5/6] feat(recipes): implement B2 recipe detail page with mobile/desktop layout Co-Authored-By: Claude Sonnet 4.6 --- .../routes/(app)/recipes/[id]/+page.svelte | 38 ++++++++++ .../routes/(app)/recipes/[id]/page.test.ts | 69 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 frontend/src/routes/(app)/recipes/[id]/+page.svelte create mode 100644 frontend/src/routes/(app)/recipes/[id]/page.test.ts diff --git a/frontend/src/routes/(app)/recipes/[id]/+page.svelte b/frontend/src/routes/(app)/recipes/[id]/+page.svelte new file mode 100644 index 0000000..2c19e8a --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/+page.svelte @@ -0,0 +1,38 @@ + + + + {data.recipe.name} — Mealplan + + +
+ + +
+
+ +
+
+ +
+
+
diff --git a/frontend/src/routes/(app)/recipes/[id]/page.test.ts b/frontend/src/routes/(app)/recipes/[id]/page.test.ts new file mode 100644 index 0000000..f599700 --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/page.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import Page from './+page.svelte'; + +const mockData = { + recipe: { + id: 'r1', + name: 'Spaghetti Bolognese', + serves: 4, + cookTimeMin: 30, + effort: 'Easy', + heroImageUrl: undefined as string | undefined, + ingredients: [ + { ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' }, + { ingredientId: 'i2', name: 'Hackfleisch', quantity: 400, unit: 'g' } + ], + steps: [ + { stepNumber: 1, instruction: 'Wasser aufsetzen' }, + { stepNumber: 2, instruction: 'Sauce zubereiten' } + ], + tags: [{ id: 't1', name: 'Pasta', tagType: 'category' }] + } +}; + +describe('recipe detail page', () => { + it('renders the recipe name', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText('Spaghetti Bolognese')).toBeInTheDocument(); + }); + + it('renders page title', () => { + render(Page, { props: { data: mockData } }); + expect(document.title).toBe('Spaghetti Bolognese — Mealplan'); + }); + + it('renders back link to /recipes', () => { + render(Page, { props: { data: mockData } }); + const backLink = screen.getByRole('link', { name: /zurück/i }); + expect(backLink).toHaveAttribute('href', '/recipes'); + }); + + it('renders cook now link to /cook/[id]', () => { + render(Page, { props: { data: mockData } }); + const cookLink = screen.getByRole('link', { name: /jetzt kochen/i }); + expect(cookLink).toHaveAttribute('href', '/cook/r1'); + }); + + it('renders ingredients section heading', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText(/zutaten/i)).toBeInTheDocument(); + }); + + it('renders steps section heading', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText(/zubereitung/i)).toBeInTheDocument(); + }); + + it('renders ingredient names', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText('Spaghetti')).toBeInTheDocument(); + expect(screen.getByText('Hackfleisch')).toBeInTheDocument(); + }); + + it('renders step instructions', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText('Wasser aufsetzen')).toBeInTheDocument(); + expect(screen.getByText('Sauce zubereiten')).toBeInTheDocument(); + }); +}); From 0256b4360bdc8ddeff8bac00b78d01ff2740b952 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 10:07:19 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(recipes):=20address=20B2=20review=20?= =?UTF-8?q?=E2=80=94=20tags,=20sort,=20edit=20link,=20types,=20a11y,=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RecipeHero: render tag pills, min-h-[200px/240px], fix back link styling, remove font-[400] - IngredientList: sort by sortOrder ascending - StepList: aria-hidden on step circles - types.ts: add Tag, Ingredient, Step, RecipeDetail shared types - +page.svelte: add Edit link → /recipes/[id]/edit (desktop topbar) - Tests: tag pills, sortOrder sort, edit link, image variant, 403-as-404 documented Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/recipes/IngredientList.svelte | 14 ++++----- .../src/lib/recipes/IngredientList.test.ts | 13 ++++++++ frontend/src/lib/recipes/RecipeHero.svelte | 31 ++++++++++--------- frontend/src/lib/recipes/RecipeHero.test.ts | 21 +++++++++++++ frontend/src/lib/recipes/StepList.svelte | 3 +- frontend/src/lib/recipes/types.ts | 31 +++++++++++++++++++ .../routes/(app)/recipes/[id]/+page.svelte | 26 +++++++--------- .../(app)/recipes/[id]/page.server.test.ts | 9 ++++++ .../routes/(app)/recipes/[id]/page.test.ts | 23 ++++++++++++++ 9 files changed, 133 insertions(+), 38 deletions(-) diff --git a/frontend/src/lib/recipes/IngredientList.svelte b/frontend/src/lib/recipes/IngredientList.svelte index 3a15099..0631db4 100644 --- a/frontend/src/lib/recipes/IngredientList.svelte +++ b/frontend/src/lib/recipes/IngredientList.svelte @@ -1,13 +1,11 @@
@@ -18,7 +16,7 @@
    - {#each ingredients as ingredient (ingredient.ingredientId ?? ingredient.name)} + {#each sortedIngredients as ingredient (ingredient.ingredientId ?? ingredient.name)}
  • {#if ingredient.quantity != null} diff --git a/frontend/src/lib/recipes/IngredientList.test.ts b/frontend/src/lib/recipes/IngredientList.test.ts index 927348a..ffc2c95 100644 --- a/frontend/src/lib/recipes/IngredientList.test.ts +++ b/frontend/src/lib/recipes/IngredientList.test.ts @@ -41,4 +41,17 @@ describe('IngredientList', () => { render(IngredientList, { props: { ingredients: [] } }); expect(screen.getByText(/zutaten/i)).toBeInTheDocument(); }); + + it('renders ingredients sorted by sortOrder', () => { + const unsorted = [ + { ingredientId: 'i3', name: 'Oregano', quantity: 1, unit: 'TL', sortOrder: 3 }, + { ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g', sortOrder: 1 }, + { ingredientId: 'i2', name: 'Hackfleisch', quantity: 400, unit: 'g', sortOrder: 2 } + ]; + render(IngredientList, { props: { ingredients: unsorted } }); + const spans = document.querySelectorAll('li span:last-child'); + expect(spans[0].textContent).toBe('Spaghetti'); + expect(spans[1].textContent).toBe('Hackfleisch'); + expect(spans[2].textContent).toBe('Oregano'); + }); }); diff --git a/frontend/src/lib/recipes/RecipeHero.svelte b/frontend/src/lib/recipes/RecipeHero.svelte index 9674f6c..19c7144 100644 --- a/frontend/src/lib/recipes/RecipeHero.svelte +++ b/frontend/src/lib/recipes/RecipeHero.svelte @@ -1,17 +1,17 @@ @@ -25,6 +12,15 @@
    + +
    diff --git a/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts index a5dc7bb..8eeebd4 100644 --- a/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts +++ b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts @@ -54,4 +54,13 @@ describe('recipe detail page — load', () => { load({ fetch: vi.fn(), params: { id: 'nonexistent' } } as any) ).rejects.toMatchObject({ status: 404 }); }); + + it('throws 404 error when API returns 403 (different household — intentional)', async () => { + // Security design: we return 404 for both "not found" and "forbidden" + // to avoid revealing resource existence to unauthorized users + mockGet.mockResolvedValue({ data: undefined, error: { status: 403 } }); + await expect( + load({ fetch: vi.fn(), params: { id: 'r-other-household' } } as any) + ).rejects.toMatchObject({ status: 404 }); + }); }); diff --git a/frontend/src/routes/(app)/recipes/[id]/page.test.ts b/frontend/src/routes/(app)/recipes/[id]/page.test.ts index f599700..c25139c 100644 --- a/frontend/src/routes/(app)/recipes/[id]/page.test.ts +++ b/frontend/src/routes/(app)/recipes/[id]/page.test.ts @@ -66,4 +66,27 @@ describe('recipe detail page', () => { expect(screen.getByText('Wasser aufsetzen')).toBeInTheDocument(); expect(screen.getByText('Sauce zubereiten')).toBeInTheDocument(); }); + + it('renders edit link to /recipes/[id]/edit', () => { + render(Page, { props: { data: mockData } }); + const editLink = screen.getByRole('link', { name: /bearbeiten/i }); + expect(editLink).toHaveAttribute('href', '/recipes/r1/edit'); + }); + + it('renders tag pills in hero', () => { + render(Page, { props: { data: mockData } }); + expect(screen.getByText('Pasta')).toBeInTheDocument(); + }); + + it('renders hero image when heroImageUrl is provided', () => { + render(Page, { + props: { + data: { + recipe: { ...mockData.recipe, heroImageUrl: '/uploads/pasta.jpg' } + } + } + }); + const img = screen.getByRole('img', { name: /spaghetti bolognese/i }); + expect(img).toHaveAttribute('src', '/uploads/pasta.jpg'); + }); });