feat(journey-editor): JourneyEditor frontend — issue #753 #792

Merged
marcel merged 92 commits from feat/issue-753-journey-editor into feat/issue-750-lesereisen-data-model 2026-06-11 12:07:23 +02:00
Showing only changes of commit 06b8c99ce7 - Show all commits

View File

@@ -192,27 +192,92 @@ describe('JourneyEditor — remove with rollback', () => {
render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) }));
// Click remove (no note → direct remove)
await userEvent.click(page.getByRole('button', { name: 'Wirklich entfernen?' }));
await userEvent.click(page.getByRole('button', { name: 'Eintrag entfernen' }));
await new Promise((r) => setTimeout(r, 50));
// Item should be restored after rollback
await expect.element(page.getByText('Brief A')).toBeInTheDocument();
});
it('item-add does NOT mark dirty (isDirty stays false)', async () => {
it('item-add enables publish button (isDirty stays false, canPublish becomes true)', async () => {
const newItem = { id: 'i1', position: 0, note: 'Test' };
mockCsrfFetch(() => newItem);
const onSubmit = vi.fn().mockResolvedValue(undefined);
render(JourneyEditor, defaultProps({ onSubmit }));
render(JourneyEditor, defaultProps());
// Add interlude (no unsaved warning should interfere)
// Publish should be disabled before adding item
await expect.element(page.getByRole('button', { name: /Veröffentlichen/ })).toBeDisabled();
// Add interlude
await userEvent.click(page.getByText('Zwischentext hinzufügen'));
await userEvent.fill(page.getByPlaceholder('Zwischentext eingeben…'), 'Test');
await userEvent.click(page.getByRole('button', { name: 'Hinzufügen', exact: true }));
await new Promise((r) => setTimeout(r, 50));
// Saving (which requires non-empty title)no unsaved warning dialog
await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
// After item add, publish becomes enableditem was added and state is correct
await expect.element(page.getByRole('button', { name: /Veröffentlichen/ })).not.toBeDisabled();
});
});
describe('JourneyEditor — reorder via move buttons', () => {
it('move-up calls PUT reorder with swapped IDs', async () => {
const items = [
{ id: 'i1', position: 0, document: docSummary('d1', 'Brief A') },
{ id: 'i2', position: 1, document: docSummary('d2', 'Brief B') }
];
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue([
{ id: 'i2', position: 0, document: docSummary('d2', 'Brief B') },
{ id: 'i1', position: 1, document: docSummary('d1', 'Brief A') }
])
})
);
render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) }));
await userEvent.click(page.getByRole('button', { name: /Brief B.*nach oben verschieben/ }));
await new Promise((r) => setTimeout(r, 50)); // handleMoveUp → handleReorder → csrfFetch: two await levels
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.stringContaining('/items/reorder'),
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ itemIds: ['i2', 'i1'] })
})
);
});
it('move-down calls PUT reorder with swapped IDs', async () => {
const items = [
{ id: 'i1', position: 0, document: docSummary('d1', 'Brief A') },
{ id: 'i2', position: 1, document: docSummary('d2', 'Brief B') }
];
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue([
{ id: 'i2', position: 0, document: docSummary('d2', 'Brief B') },
{ id: 'i1', position: 1, document: docSummary('d1', 'Brief A') }
])
})
);
render(JourneyEditor, defaultProps({ geschichte: makeGeschichte({ items }) }));
await userEvent.click(page.getByRole('button', { name: /Brief A.*nach unten verschieben/ }));
await new Promise((r) => setTimeout(r, 50)); // handleMoveDown → handleReorder → csrfFetch: two await levels
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.stringContaining('/items/reorder'),
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ itemIds: ['i2', 'i1'] })
})
);
});
});