feat(journey-editor): build JourneyItemRow with note editing and remove confirm
Item row with drag handle, move-up/down buttons, inline note textarea (PATCH on blur), interlude visual treatment, and inline confirm for removes that would discard a note. Interlude note cannot be cleared (blocked on empty). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
112
frontend/src/lib/geschichte/JourneyItemRow.svelte.spec.ts
Normal file
112
frontend/src/lib/geschichte/JourneyItemRow.svelte.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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 — 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: 'Wirklich 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: 'Wirklich 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: 'Wirklich entfernen?' }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user