feat(planner): add remove meal with undo; fix RecipePicker badge for neutral delta

- MealActionSheet: new onremove prop + Entfernen button (guarded by #if)
- +page.svelte: handleRemoveMeal submits delete form, shows undo bar;
  undo re-adds via addSlot form; refactored handleUndo to undoCallback
  pattern; desktop day-detail panel also gets Entfernen button
- RecipePicker: only show green +delta badge when scoreDelta > 0;
  neutral (scoreDelta = 0) shows no badge instead of ⚠ Variationskonflikt
- Tests: page.test.ts remove-meal describe, RecipePicker neutral badge test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 12:47:53 +02:00
parent 77cdccb26c
commit 1de9dfc314
4 changed files with 96 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/svelte';
import { render, screen, waitFor, within } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import Page from './+page.svelte';
@@ -13,13 +13,23 @@ const PLAN_ID = 'plan-00000000-0000-0000-0000-000000000001';
const DATE = '2025-01-06'; // Monday, January 6 2025
const mockData = {
weekPlan: { id: PLAN_ID, weekStart: DATE, status: 'draft', slots: [] },
weekPlan: { id: PLAN_ID, weekStart: DATE, status: 'draft', slots: [] as any[] },
varietyScore: null,
weekStart: DATE,
recipes: [{ id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 }],
benutzer: { rolle: 'planer' }
};
const mockDataWithSlot = {
...mockData,
weekPlan: {
id: PLAN_ID,
weekStart: DATE,
status: 'draft',
slots: [{ id: 'slot-1', slotDate: DATE, recipe: { id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 } }]
}
};
const mockSuggestions = [
{
recipe: { id: 's1', name: 'Lachsfilet', effort: 'easy', cookTimeMin: 20 },
@@ -82,3 +92,35 @@ describe('+page.svelte — $effect suggestion fetch', () => {
expect(fetchOptions?.signal).toBeInstanceOf(AbortSignal);
});
});
describe('+page.svelte — remove meal', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('clicking Entfernen in MealActionSheet shows undo bar with recipe name', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve({ suggestions: [] }) }));
render(Page, { props: { data: mockDataWithSlot } });
await userEvent.click(screen.getByTestId('day-meal-card'));
await userEvent.click(await screen.findByRole('button', { name: /Entfernen/i }));
const undoBar = screen.getByTestId('undo-bar');
expect(undoBar).toBeTruthy();
expect(within(undoBar).getByText(/Beef Bourguignon/)).toBeTruthy();
});
it('clicking Rückgängig after remove hides the undo bar', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve({ suggestions: [] }) }));
render(Page, { props: { data: mockDataWithSlot } });
await userEvent.click(screen.getByTestId('day-meal-card'));
await userEvent.click(await screen.findByRole('button', { name: /Entfernen/i }));
await userEvent.click(screen.getByRole('button', { name: /Rückgängig/i }));
expect(screen.queryByTestId('undo-bar')).toBeNull();
});
});