From 2b7a7cceecffc5788fa023216430903c96ee90aa Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 10 Apr 2026 10:47:19 +0200 Subject: [PATCH] feat(planner): add EmptyDayTile component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashed-border empty slot tile with + Gericht wählen CTA and lazy reasoning tags (Neues Protein, Aufwand: leicht) derived from topSuggestion prop via computeReasoningTags. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/planner/EmptyDayTile.svelte | 89 +++++++++++++++++++ frontend/src/lib/planner/EmptyDayTile.test.ts | 88 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 frontend/src/lib/planner/EmptyDayTile.svelte create mode 100644 frontend/src/lib/planner/EmptyDayTile.test.ts diff --git a/frontend/src/lib/planner/EmptyDayTile.svelte b/frontend/src/lib/planner/EmptyDayTile.svelte new file mode 100644 index 0000000..fed8d2f --- /dev/null +++ b/frontend/src/lib/planner/EmptyDayTile.svelte @@ -0,0 +1,89 @@ + + +
+ {#if isPlanner} + + {/if} + + {#if topSuggestion} +

+ {topSuggestion.recipe.name} +

+ + {#if reasoningTags.length > 0} +
+ {#each reasoningTags as tag (tag.id)} + + {tag.label} + + {/each} +
+ {/if} + {/if} +
diff --git a/frontend/src/lib/planner/EmptyDayTile.test.ts b/frontend/src/lib/planner/EmptyDayTile.test.ts new file mode 100644 index 0000000..fa52d9b --- /dev/null +++ b/frontend/src/lib/planner/EmptyDayTile.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; +import EmptyDayTile from './EmptyDayTile.svelte'; + +const slotDate = '2026-04-14'; +const slotId = 'slot-1'; + +const topSuggestionNewProtein = { + recipe: { + id: 'r1', + name: 'Lachs mit Gemüse', + cookTimeMin: 20, + effort: 'einfach', + tags: [{ id: 't1', name: 'Fisch', tagType: 'protein' }] + }, + scoreDelta: 3.2, + hasConflict: false +}; + +const slotMapEmpty = {}; + +describe('EmptyDayTile', () => { + describe('base render', () => { + it('shows + CTA for planner', () => { + render(EmptyDayTile, { props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty } }); + expect(screen.getByRole('button', { name: /Gericht wählen/i })).toBeTruthy(); + }); + + it('hides + CTA for non-planner', () => { + render(EmptyDayTile, { props: { slotDate, slotId, isPlanner: false, slotMap: slotMapEmpty } }); + expect(screen.queryByRole('button', { name: /Gericht wählen/i })).toBeNull(); + }); + + it('calls onaddrecipe when + CTA clicked', async () => { + const onaddrecipe = vi.fn(); + const user = userEvent.setup(); + render(EmptyDayTile, { props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty, onaddrecipe } }); + await user.click(screen.getByRole('button', { name: /Gericht wählen/i })); + expect(onaddrecipe).toHaveBeenCalledOnce(); + }); + + it('has data-testid="empty-day-tile"', () => { + render(EmptyDayTile, { props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty } }); + expect(screen.getByTestId('empty-day-tile')).toBeTruthy(); + }); + }); + + describe('reasoning tags', () => { + it('shows no tags when no topSuggestion', () => { + render(EmptyDayTile, { props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty } }); + expect(screen.queryByTestId('reasoning-tag')).toBeNull(); + }); + + it('shows Neues Protein tag when topSuggestion has new protein', () => { + render(EmptyDayTile, { + props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty, topSuggestion: topSuggestionNewProtein } + }); + expect(screen.getByText('Neues Protein')).toBeTruthy(); + }); + + it('shows Aufwand tag for easy suggestion', () => { + render(EmptyDayTile, { + props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty, topSuggestion: topSuggestionNewProtein } + }); + expect(screen.getByText('Aufwand: leicht')).toBeTruthy(); + }); + + it('shows suggestion recipe name when topSuggestion provided', () => { + render(EmptyDayTile, { + props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty, topSuggestion: topSuggestionNewProtein } + }); + expect(screen.getByText('Lachs mit Gemüse')).toBeTruthy(); + }); + + it('does not show tags when suggestion has no matching conditions', () => { + const heavySuggestion = { + recipe: { id: 'r2', name: 'Roulade', cookTimeMin: 120, effort: 'aufwändig', tags: [] }, + scoreDelta: 1.0, + hasConflict: false + }; + render(EmptyDayTile, { + props: { slotDate, slotId, isPlanner: true, slotMap: slotMapEmpty, topSuggestion: heavySuggestion } + }); + expect(screen.queryByTestId('reasoning-tag')).toBeNull(); + }); + }); +});