feat(recipes): add StepList component with numbered circles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
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