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(); }); });