Files
familienarchiv/frontend/src/lib/geschichte/JourneyItemRow.svelte.spec.ts
2026-06-09 17:51:49 +02:00

147 lines
5.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import JourneyItemRow from './JourneyItemRow.svelte';
const docItem = (overrides: Partial<{ note: string }> = {}) => ({
id: 'item-1',
position: 0,
document: { id: 'doc-1', title: 'Brief von Karl', datePrecision: 'DAY' as const },
...overrides
});
const interludeItem = (note = 'Reise nach Wien') => ({
id: 'item-2',
position: 1,
note
});
const defaultProps = (overrides = {}) => ({
index: 0,
total: 3,
onMoveUp: vi.fn(),
onMoveDown: vi.fn(),
onRemove: vi.fn(),
onNotePatch: vi.fn().mockResolvedValue(undefined),
...overrides
});
afterEach(() => cleanup());
describe('JourneyItemRow — note textarea', () => {
it('opens note textarea on "Notiz hinzufügen" click', async () => {
render(JourneyItemRow, { item: docItem(), ...defaultProps() });
await userEvent.click(page.getByText('Notiz hinzufügen'));
await expect
.element(page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ }))
.toBeInTheDocument();
});
it('calls onNotePatch on textarea blur with non-empty value', async () => {
const onNotePatch = vi.fn().mockResolvedValue(undefined);
render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });
await userEvent.click(page.getByText('Notiz hinzufügen'));
const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
await userEvent.fill(textarea, 'Eine neue Notiz');
await textarea.element().dispatchEvent(new FocusEvent('blur'));
expect(onNotePatch).toHaveBeenCalledWith('Eine neue Notiz');
});
});
describe('JourneyItemRow — note error state', () => {
it('shows role=alert error message when onNotePatch rejects', async () => {
const onNotePatch = vi.fn().mockRejectedValue(new Error('server error'));
render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });
await userEvent.click(page.getByText('Notiz hinzufügen'));
const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
await userEvent.fill(textarea, 'Eine Notiz');
await textarea.element().dispatchEvent(new FocusEvent('blur'));
await expect.element(page.getByRole('alert')).toBeInTheDocument();
});
});
describe('JourneyItemRow — note remove error state', () => {
it('restores note and shows error when onNotePatch rejects during remove', async () => {
const onNotePatch = vi.fn().mockRejectedValue(new Error('server error'));
render(JourneyItemRow, {
item: docItem({ note: 'keep me' }),
...defaultProps({ onNotePatch })
});
await userEvent.click(page.getByText('Notiz entfernen'));
await new Promise((r) => setTimeout(r, 50));
// textarea should be visible again (showNote restored)
await expect
.element(page.getByRole('textbox', { name: /Kuratoren-Notiz/ }))
.toBeInTheDocument();
// error alert should be shown
await expect.element(page.getByRole('alert')).toBeInTheDocument();
});
});
describe('JourneyItemRow — interlude rules', () => {
it('does not show "Notiz entfernen" for interlude items', async () => {
render(JourneyItemRow, { item: interludeItem(), ...defaultProps() });
// Note section should be visible (interlude always shows note)
await expect
.element(page.getByRole('textbox', { name: /Kuratoren-Notiz/ }))
.toBeInTheDocument();
// But "Notiz entfernen" must be absent
await expect.element(page.getByText('Notiz entfernen')).not.toBeInTheDocument();
});
it('blocks saving empty text on interlude note blur', async () => {
const onNotePatch = vi.fn().mockResolvedValue(undefined);
render(JourneyItemRow, {
item: interludeItem('original text'),
...defaultProps({ onNotePatch })
});
const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz/ });
await userEvent.clear(textarea);
await textarea.element().dispatchEvent(new FocusEvent('blur'));
expect(onNotePatch).not.toHaveBeenCalled();
});
});
describe('JourneyItemRow — remove confirm', () => {
it('shows inline confirm when removing a document item that has a note', async () => {
render(JourneyItemRow, {
item: docItem({ note: 'Wichtige Notiz' }),
...defaultProps()
});
// Click remove (x button)
await userEvent.click(page.getByRole('button', { name: 'Eintrag entfernen' }));
await expect.element(page.getByText('Wirklich entfernen?')).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: 'Bestätigen' })).toBeInTheDocument();
});
it('confirm cancel restores remove button without calling onRemove', async () => {
const onRemove = vi.fn();
render(JourneyItemRow, {
item: docItem({ note: 'Notiz' }),
...defaultProps({ onRemove })
});
await userEvent.click(page.getByRole('button', { name: 'Eintrag entfernen' }));
await userEvent.click(page.getByRole('button', { name: 'Abbrechen' }));
expect(onRemove).not.toHaveBeenCalled();
// The remove button should be back
await expect
.element(page.getByRole('button', { name: 'Eintrag entfernen' }))
.toBeInTheDocument();
});
});