feat(staples): A3/D3 — Pantry staples toggle UI #35

Merged
marcel merged 17 commits from feat/issue-20-pantry-staples into master 2026-04-03 09:35:03 +02:00
2 changed files with 127 additions and 3 deletions
Showing only changes of commit 97175e7d9d - Show all commits

View File

@@ -2,6 +2,44 @@
<title>Vorräte einrichten — Mealplan</title>
</svelte:head>
<div class="flex min-h-screen items-center justify-center bg-[var(--color-page)]">
<p class="text-[var(--color-text-muted)]">A3 — Vorräte einrichten (coming soon)</p>
</div>
<script lang="ts">
import ProgressSidebar from '$lib/components/ProgressSidebar.svelte';
import StaplesManager from '$lib/onboarding/StaplesManager.svelte';
type Category = { id: string; name: string; ingredients: { id: string; name: string; isStaple: boolean }[] };
let { data, ctx }: { data: { categories: Category[] }; ctx?: string } = $props();
const isOnboarding = $derived(ctx === 'onboarding');
</script>
{#if isOnboarding}
<div class="flex min-h-screen bg-[var(--color-page)]">
<!-- Desktop sidebar -->
<aside class="hidden md:flex w-[300px] flex-shrink-0 flex-col bg-[var(--color-surface)] border-r border-[var(--color-border)] p-[40px_28px]">
<ProgressSidebar currentStep={2} />
</aside>
<!-- Main area -->
<main class="flex flex-1 flex-col">
<!-- Mobile step indicator -->
<p class="md:hidden px-6 pt-6 text-sm text-[var(--color-text-muted)]">Schritt 2 von 3</p>
<!-- Content -->
<div class="flex-1 p-6">
<StaplesManager categories={data.categories} context="onboarding" />
</div>
<!-- Footer navigation -->
<div class="flex justify-between p-6">
<a href="/planner">Überspringen</a>
<a href="/household/invite">Weiter</a>
</div>
</main>
</div>
{:else}
<div class="flex min-h-screen flex-col bg-[var(--color-page)]">
<h1>Vorräte</h1>
<StaplesManager categories={data.categories} context="settings" />
</div>
{/if}

View File

@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import Page from './+page.svelte';
vi.mock('$app/state', () => ({
page: { url: { searchParams: { get: vi.fn() } } }
}));
const mockCategories = [
{
id: 'cat-1',
name: 'Öle & Fette',
ingredients: [
{ id: 'ing-1', name: 'Olivenöl', isStaple: true },
{ id: 'ing-2', name: 'Butter', isStaple: false }
]
}
];
describe('staples page — onboarding context (?ctx=onboarding)', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('renders ProgressSidebar with step 2 active', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step');
});
it('renders Continue button linking to /household/invite', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
const continueLink = screen.getByRole('link', { name: /weiter/i });
expect(continueLink).toHaveAttribute('href', '/household/invite');
});
it('renders Skip button linking to /planner', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
const skipLink = screen.getByRole('link', { name: /überspringen/i });
expect(skipLink).toHaveAttribute('href', '/planner');
});
it('renders the StaplesManager with categories', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
});
it('sets the page title', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
expect(document.title).toBe('Vorräte einrichten — Mealplan');
});
it('renders mobile step indicator Schritt 2 von 3', () => {
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument();
});
});
describe('staples page — settings context (no ctx)', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('does not render ProgressSidebar', () => {
render(Page, { props: { data: { categories: mockCategories } } });
expect(screen.queryByTestId('step-1')).not.toBeInTheDocument();
});
it('does not render Continue or Skip buttons', () => {
render(Page, { props: { data: { categories: mockCategories } } });
expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument();
});
it('renders a settings heading', () => {
render(Page, { props: { data: { categories: mockCategories } } });
expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument();
});
});