feat(recipes): B2 — Recipe detail view with hero, ingredients, steps #37
32
frontend/src/lib/recipes/StepList.svelte
Normal file
32
frontend/src/lib/recipes/StepList.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
type Step = { stepNumber?: number; instruction?: string };
|
||||
|
||||
let { steps }: { steps: Step[] } = $props();
|
||||
|
||||
const sortedSteps = $derived(
|
||||
[...steps].sort((a, b) => (a.stepNumber ?? 0) - (b.stepNumber ?? 0))
|
||||
);
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2
|
||||
class="text-[12px] font-medium font-sans tracking-[0.08em] uppercase text-[var(--color-text-muted)] mb-[16px]"
|
||||
>
|
||||
Zubereitung
|
||||
</h2>
|
||||
<ol>
|
||||
{#each sortedSteps as step (step.stepNumber)}
|
||||
<li class="flex gap-[16px] items-start mb-[20px]">
|
||||
<div
|
||||
data-testid="step-circle"
|
||||
class="w-[28px] h-[28px] rounded-full bg-[var(--green-dark)] text-white flex items-center justify-center shrink-0 font-sans text-[13px] font-medium"
|
||||
>
|
||||
{step.stepNumber}
|
||||
</div>
|
||||
<p class="text-[14px] text-[var(--color-text)] leading-[1.6] pt-[4px]">
|
||||
{step.instruction}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</section>
|
||||
50
frontend/src/lib/recipes/StepList.test.ts
Normal file
50
frontend/src/lib/recipes/StepList.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import StepList from './StepList.svelte';
|
||||
|
||||
const mockSteps = [
|
||||
{ stepNumber: 1, instruction: 'Wasser zum Kochen bringen' },
|
||||
{ stepNumber: 2, instruction: 'Spaghetti al dente kochen' },
|
||||
{ stepNumber: 3, instruction: 'Sauce bereiten' }
|
||||
];
|
||||
|
||||
describe('StepList', () => {
|
||||
it('renders the section heading', () => {
|
||||
render(StepList, { props: { steps: mockSteps } });
|
||||
expect(screen.getByText(/zubereitung/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders each step instruction', () => {
|
||||
render(StepList, { props: { steps: mockSteps } });
|
||||
expect(screen.getByText('Wasser zum Kochen bringen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Spaghetti al dente kochen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sauce bereiten')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders step numbers', () => {
|
||||
render(StepList, { props: { steps: mockSteps } });
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders numbered circles with step numbers', () => {
|
||||
render(StepList, { props: { steps: mockSteps } });
|
||||
const circles = document.querySelectorAll('[data-testid="step-circle"]');
|
||||
expect(circles).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('renders steps in stepNumber order', () => {
|
||||
const shuffled = [mockSteps[2], mockSteps[0], mockSteps[1]];
|
||||
render(StepList, { props: { steps: shuffled } });
|
||||
const circles = document.querySelectorAll('[data-testid="step-circle"]');
|
||||
expect(circles[0].textContent).toBe('1');
|
||||
expect(circles[1].textContent).toBe('2');
|
||||
expect(circles[2].textContent).toBe('3');
|
||||
});
|
||||
|
||||
it('renders empty state when no steps', () => {
|
||||
render(StepList, { props: { steps: [] } });
|
||||
expect(screen.getByText(/zubereitung/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user