feat(staples): add CategorySection component with eyebrow heading and chip row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
26
frontend/src/lib/onboarding/CategorySection.svelte
Normal file
26
frontend/src/lib/onboarding/CategorySection.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import StapleChip from './StapleChip.svelte';
|
||||
|
||||
type Ingredient = { id: string; name: string; isStaple: boolean };
|
||||
|
||||
let { name, ingredients, onToggle }: {
|
||||
name: string;
|
||||
ingredients: Ingredient[];
|
||||
onToggle: (id: string, value: boolean) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="text-[10px] font-medium tracking-[.08em] uppercase text-[var(--color-text-muted)] mb-[8px]">
|
||||
{name}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-[6px]">
|
||||
{#each ingredients as ingredient (ingredient.id)}
|
||||
<StapleChip
|
||||
name={ingredient.name}
|
||||
selected={ingredient.isStaple}
|
||||
onToggle={(value) => onToggle(ingredient.id, value)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
55
frontend/src/lib/onboarding/CategorySection.test.ts
Normal file
55
frontend/src/lib/onboarding/CategorySection.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import CategorySection from './CategorySection.svelte';
|
||||
|
||||
const mockIngredients = [
|
||||
{ id: '1', name: 'Olivenöl', isStaple: true },
|
||||
{ id: '2', name: 'Butter', isStaple: false },
|
||||
{ id: '3', name: 'Kokosöl', isStaple: false }
|
||||
];
|
||||
|
||||
describe('CategorySection', () => {
|
||||
it('renders the category name as a heading', () => {
|
||||
render(CategorySection, {
|
||||
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
|
||||
});
|
||||
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a chip for each ingredient', () => {
|
||||
render(CategorySection, {
|
||||
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'Olivenöl' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Butter' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Kokosöl' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reflects isStaple state on each chip', () => {
|
||||
render(CategorySection, {
|
||||
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle: vi.fn() }
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'Olivenöl' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Butter' })).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
|
||||
it('calls onToggle with ingredient id and new value when chip is clicked', async () => {
|
||||
const { userEvent } = await import('@testing-library/user-event');
|
||||
const user = userEvent.setup();
|
||||
const onToggle = vi.fn();
|
||||
render(CategorySection, {
|
||||
props: { name: 'Öle & Fette', ingredients: mockIngredients, onToggle }
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Butter' }));
|
||||
expect(onToggle).toHaveBeenCalledWith('2', true);
|
||||
});
|
||||
|
||||
it('renders an empty category without crashing', () => {
|
||||
render(CategorySection, {
|
||||
props: { name: 'Leer', ingredients: [], onToggle: vi.fn() }
|
||||
});
|
||||
expect(screen.getByText('Leer')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user