test(shopping): add component tests for ShoppingHeader, ChecklistItem, AddCustomItem
25 tests covering counts, planner guard, aria-checked, strikethrough, recipe labels, expand/collapse, and form submission. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #43.
This commit is contained in:
58
frontend/src/lib/shopping/AddCustomItem.test.ts
Normal file
58
frontend/src/lib/shopping/AddCustomItem.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import { userEvent } from '@testing-library/user-event';
|
||||||
|
import AddCustomItem from './AddCustomItem.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/forms', () => ({
|
||||||
|
enhance: () => ({ destroy: () => {} })
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AddCustomItem', () => {
|
||||||
|
it('shows collapsed trigger button initially', () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
expect(screen.getByText(/Artikel hinzufügen/)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByPlaceholderText('Artikelname')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands form when trigger button is clicked', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
expect(screen.getByPlaceholderText('Artikelname')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses form when Abbrechen is clicked', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
expect(screen.getByPlaceholderText('Artikelname')).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText('Abbrechen'));
|
||||||
|
expect(screen.queryByPlaceholderText('Artikelname')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submit button is disabled when name is empty', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
expect(screen.getByRole('button', { name: /Hinzufügen/i })).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submit button is enabled when name is entered', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
await userEvent.type(screen.getByPlaceholderText('Artikelname'), 'Papier');
|
||||||
|
expect(screen.getByRole('button', { name: /Hinzufügen/i })).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('form submits to ?/addItem action', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-1' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
const form = screen.getByPlaceholderText('Artikelname').closest('form')!;
|
||||||
|
expect(form).toHaveAttribute('action', '?/addItem');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('form includes listId as hidden input', async () => {
|
||||||
|
render(AddCustomItem, { props: { listId: 'list-42' } });
|
||||||
|
await userEvent.click(screen.getByText(/Artikel hinzufügen/));
|
||||||
|
const form = screen.getByPlaceholderText('Artikelname').closest('form')!;
|
||||||
|
const listIdInput = form.querySelector('input[name="listId"]') as HTMLInputElement;
|
||||||
|
expect(listIdInput.value).toBe('list-42');
|
||||||
|
});
|
||||||
|
});
|
||||||
88
frontend/src/lib/shopping/ChecklistItem.test.ts
Normal file
88
frontend/src/lib/shopping/ChecklistItem.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import ChecklistItem from './ChecklistItem.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/forms', () => ({
|
||||||
|
enhance: () => ({ destroy: () => {} })
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ChecklistItem', () => {
|
||||||
|
const baseProps = {
|
||||||
|
listId: 'list-1',
|
||||||
|
itemId: 'item-1',
|
||||||
|
name: 'Tomaten',
|
||||||
|
quantity: null,
|
||||||
|
unit: null,
|
||||||
|
isChecked: false,
|
||||||
|
sourceRecipes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders the item name', () => {
|
||||||
|
render(ChecklistItem, { props: baseProps });
|
||||||
|
expect(screen.getByText('Tomaten')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders quantity and unit when provided', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, quantity: 3, unit: 'Stück' } });
|
||||||
|
expect(screen.getByText('3 Stück')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders quantity without unit when unit is null', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, quantity: 2, unit: null } });
|
||||||
|
expect(screen.getByText('2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies line-through style when checked', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, isChecked: true } });
|
||||||
|
const nameEl = screen.getByText('Tomaten');
|
||||||
|
expect(nameEl.className).toContain('line-through');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not apply line-through when unchecked', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, isChecked: false } });
|
||||||
|
const nameEl = screen.getByText('Tomaten');
|
||||||
|
expect(nameEl.className).not.toContain('line-through');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows recipe label for source recipes when unchecked', () => {
|
||||||
|
render(ChecklistItem, {
|
||||||
|
props: {
|
||||||
|
...baseProps,
|
||||||
|
sourceRecipes: [{ id: 'r-1', name: 'Spaghetti' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(screen.getByText(/Für: Spaghetti/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides recipe label when item is checked', () => {
|
||||||
|
render(ChecklistItem, {
|
||||||
|
props: {
|
||||||
|
...baseProps,
|
||||||
|
isChecked: true,
|
||||||
|
sourceRecipes: [{ id: 'r-1', name: 'Spaghetti' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(screen.queryByText(/Für:/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets aria-checked to false when unchecked', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, isChecked: false } });
|
||||||
|
const button = screen.getByRole('checkbox');
|
||||||
|
expect(button).toHaveAttribute('aria-checked', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets aria-checked to true when checked', () => {
|
||||||
|
render(ChecklistItem, { props: { ...baseProps, isChecked: true } });
|
||||||
|
const button = screen.getByRole('checkbox');
|
||||||
|
expect(button).toHaveAttribute('aria-checked', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes listId and itemId as hidden inputs', () => {
|
||||||
|
render(ChecklistItem, { props: baseProps });
|
||||||
|
const form = screen.getByRole('checkbox').closest('form')!;
|
||||||
|
const listIdInput = form.querySelector('input[name="listId"]') as HTMLInputElement;
|
||||||
|
const itemIdInput = form.querySelector('input[name="itemId"]') as HTMLInputElement;
|
||||||
|
expect(listIdInput.value).toBe('list-1');
|
||||||
|
expect(itemIdInput.value).toBe('item-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
71
frontend/src/lib/shopping/ShoppingHeader.test.ts
Normal file
71
frontend/src/lib/shopping/ShoppingHeader.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import ShoppingHeader from './ShoppingHeader.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/forms', () => ({
|
||||||
|
enhance: () => ({ destroy: () => {} })
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ShoppingHeader', () => {
|
||||||
|
const baseProps = {
|
||||||
|
totalItems: 0,
|
||||||
|
checkedCount: 0,
|
||||||
|
generatedAt: null,
|
||||||
|
weekPlanId: null,
|
||||||
|
isPlanner: false,
|
||||||
|
hasShoppingList: false
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders the heading', () => {
|
||||||
|
render(ShoppingHeader, { props: baseProps });
|
||||||
|
expect(screen.getByText('Einkaufsliste')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows counts when hasShoppingList is true', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, totalItems: 5, checkedCount: 2, hasShoppingList: true }
|
||||||
|
});
|
||||||
|
expect(screen.getByText(/3 Artikel übrig/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/2 abgehakt/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show counts when hasShoppingList is false', () => {
|
||||||
|
render(ShoppingHeader, { props: { ...baseProps, hasShoppingList: false } });
|
||||||
|
expect(screen.queryByText(/Artikel übrig/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows generate button for planner with weekPlanId', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, isPlanner: true, weekPlanId: 'plan-1' }
|
||||||
|
});
|
||||||
|
expect(screen.getByRole('button', { name: /Liste generieren/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows regenerate button when planner already has a list', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, isPlanner: true, weekPlanId: 'plan-1', hasShoppingList: true, totalItems: 3, checkedCount: 0 }
|
||||||
|
});
|
||||||
|
expect(screen.getByRole('button', { name: /Neu generieren/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides generate button for non-planner', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, isPlanner: false, weekPlanId: 'plan-1' }
|
||||||
|
});
|
||||||
|
expect(screen.queryByRole('button', { name: /generieren/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides generate button when weekPlanId is null', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, isPlanner: true, weekPlanId: null }
|
||||||
|
});
|
||||||
|
expect(screen.queryByRole('button', { name: /generieren/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows formatted timestamp when generatedAt is provided', () => {
|
||||||
|
render(ShoppingHeader, {
|
||||||
|
props: { ...baseProps, hasShoppingList: true, generatedAt: '2026-04-06T10:30:00Z', totalItems: 1, checkedCount: 0 }
|
||||||
|
});
|
||||||
|
expect(screen.getByText(/erstellt/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user