From 86a25eb038e87a39f4f5cb5811412831f8b29cf0 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 09:56:35 +0200 Subject: [PATCH] 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(); + }); +});