From 0256b4360bdc8ddeff8bac00b78d01ff2740b952 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 10:07:19 +0200 Subject: [PATCH] =?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'); + }); });